某病毒样本分析报告
一些碎碎念
学了这么久逆向感觉一直面向的CTF题,很少接触真正的实际应用,刚接触样本分析感觉很多都不会,找一篇写的不错的文章入个门先
[原创]样本分析核心手法:一个简单的开始-软件逆向-看雪-安全社区|安全招聘|kanxue.com
哈哈由于暑假比较摆烂,中间再插播一个工业互联网决赛,所以学的很慢,调的也比较慢,结合一些网上的文章还有一些样本分析实例勉强做了一些浅薄的分析,过程如下:
样本名称:8aca8fe2dfa143a3210f00df3f43d4185fefa7a3
分析软件:火绒剑/idapro/OllyDBG/studyPE/010Editor/exeinfo
调试环境:Windows 10 x64 22H2
行为分析
这里使用火绒剑进行行为分析
发现有很多注册表操作,对文件目录的读取和修改行为,以及网络连接行为,连接的 ip 地址经查询在保加利亚,最后还把自己给删了



查壳
之前只接触过 UPX 壳,还稍微研究过一点点源码,但是样本分析里面的查壳比想象中复杂很多,光是判断有没有壳就是一道难题

exeinfo PE 能查出来C++版本,MSVC编译,入口点,以及一些其他信息,看着不像加了壳的

但是用 studyPE 一分析导入表以及用到的 API 就觉得不对劲

只能找到两个 DLL 被链接了,而且没有跟网络相关的 API 被调用,用 ida pro 打开倒是能反编译,但是字符串乱码,有大量的数据和未探索的代码

看来是藏了东西的,我们先动态调试一下。
动态调试
从 WinMain
开始调,里面只有一个函数
看起来很大一坨,但是有用的只有几行



跳出来之后发现后面执行了一段 shellcode dword_2CC7034

F7进去发现有两个主要函数,先看第一个函数
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 sub_2E4BC2F(_DWORD *a1) { int result; int v2; _DWORD v3[8]; int v4; int v5; int v6; int v7;
*(_BYTE *)a1 = 0; v7 = 0; v4 = 48546834; a1[1] = 48546834; a1[2] = v4 + 61; v6 = sub_2E4BC90((int)&unk_D4E88, (int)&unk_D5786); v2 = sub_2E4BC90((int)&unk_D4E88, (int)&unk_348BFA); a1[4] = v6; a1[5] = v2; v5 = 0; strcpy((char *)v3, "kernel32.dll"); v5 = ((int (__stdcall *)(_DWORD *))a1[4])(v3); qmemcpy(v3, "GlobalAl", 8); v3[2] = &unk_636F6C; LOBYTE(v3[3]) = 0; a1[6] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); strcpy((char *)v3, "GetLastError"); a1[7] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); strcpy((char *)v3, "Sleep"); HIWORD(v3[1]) = 0; LOBYTE(v3[2]) = 0; a1[8] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); strcpy((char *)v3, "VirtualAlloc"); a1[9] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); strcpy((char *)v3, "CreateToolhelp32Snapshot"); a1[10] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); strcpy((char *)v3, "Module32First"); HIWORD(v3[3]) = 0; LOBYTE(v3[4]) = 0; a1[11] = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); qmemcpy(v3, "CloseHan", 8); v3[2] = &unk_656C64; LOBYTE(v3[3]) = 0; result = ((int (__stdcall *)(int, _DWORD *))a1[5])(v5, v3); a1[12] = result; return result; }
|
执行完发现这个函数的作用是把kernel32里面的很多函数的地址存到了一个表里

我们恢复成结构体,恢复完以后代码就很清晰了,再查一下 MSDN 查看每个API的作用
API 名称 |
作用 |
用法示例 |
LoadLibraryA |
加载一个动态链接库(DLL)到当前进程的地址空间。 |
HMODULE hModule = LoadLibraryA("user32.dll"); |
GetProcAddress |
从指定的 DLL 中获取一个函数的地址。 |
FARPROC lpFunc = GetProcAddress(hModule, "MessageBoxA"); |
GlobalAlloc |
分配全局内存。这个函数已过时,推荐使用 HeapAlloc 。 |
HGLOBAL hMem = GlobalAlloc(GHND, dwBytes); |
GetLastError |
返回调用最近一个函数的错误代码。 |
DWORD dwError = GetLastError(); |
Sleep |
暂停当前线程的执行指定的毫秒数。 |
Sleep(1000); // 暂停1秒 |
VirtualAlloc |
在进程的虚拟地址空间中分配内存。 |
LPVOID lpAddress = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); |
CreateToolhelp32Snapshot |
创建系统快照,包含系统进程、线程、模块和使用的堆信息。 |
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); |
Module32First |
检索系统中第一个模块的信息(结合 CreateToolhelp32Snapshot 使用)。 |
Module32First(hSnapshot, &me32); |
CloseHandle |
关闭一个内核对象的句柄。 |
CloseHandle(hHandle); |
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
| void *__cdecl sub_2E4BC2F(struc_19FE98 *a1) { void *result; int v2; _DWORD v3[8]; int v4; int v5; void *v6; int v7;
LOBYTE(a1->field_0) = 0; v7 = 0; v4 = 48546834; a1->field_4 = 48546834; a1->field_8 = (void *)(v4 + 61); v6 = (void *)sub_2E4BC90((int)&unk_D4E88, (int)&unk_D5786); v2 = sub_2E4BC90((int)&unk_D4E88, (int)&unk_348BFA); a1->field_10 = v6; a1->kernel32_LoadLibraryA = (void *)v2; v5 = 0; strcpy((char *)v3, "kernel32.dll"); v5 = ((int (__stdcall *)(_DWORD *))a1->field_10)(v3); qmemcpy(v3, "GlobalAl", 8); v3[2] = &unk_636F6C; LOBYTE(v3[3]) = 0; a1->kernel32_GetProcAddress = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); strcpy((char *)v3, "GetLastError"); a1->kernel32_GlobalAlloc = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); strcpy((char *)v3, "Sleep"); HIWORD(v3[1]) = 0; LOBYTE(v3[2]) = 0; a1->kernel32_GetLastError = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); strcpy((char *)v3, "VirtualAlloc"); a1->kernel32_Sleep = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); strcpy((char *)v3, "CreateToolhelp32Snapshot"); a1->kernel32_VirtualAlloc = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); strcpy((char *)v3, "Module32First"); HIWORD(v3[3]) = 0; LOBYTE(v3[4]) = 0; a1->kernel32_CreateToolhelp32Snapshot = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); qmemcpy(v3, "CloseHan", 8); v3[2] = &unk_656C64; LOBYTE(v3[3]) = 0; result = (void *)((int (__stdcall *)(int, _DWORD *))a1->kernel32_LoadLibraryA)(v5, v3); a1->kernel32_Module32First = result; return result; }
|
然后进到后面的函数里面,发现调用了kernel32_VirtualAlloc, kernel32_CreateToolhelp32Snapshot
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int __cdecl sub_2E4C3B3(struc_19FE98 *a1) { unsigned int i; int v2; int v4[137];
v4[0] = 548; for ( i = 0; i < 0x64; ++i ) { if ( i ) ((void (__stdcall *)(int))a1->kernel32_GetLastError)(100); v2 = ((int (__stdcall *)(int, _DWORD))a1->kernel32_VirtualAlloc)(8, 0); if ( v2 != -1 ) break; if ( ((int (*)(void))a1->kernel32_GlobalAlloc)() != 24 ) break; } if ( ((int (__stdcall *)(int, int *))a1->kernel32_CreateToolhelp32Snapshot)(v2, v4) ) ((void (__cdecl *)(struc_19FE98 *))sub_2E4C072)(a1); return ((int (__stdcall *)(int))a1->kernel32_Module32First)(v2); }
|
调进sub_2E4C072
函数,前面两个函数不知道在干嘛,像是在对一堆数据做解密或者压缩操作,我们接着往后面调

到下面这个跳转,然后再创建函数继续分析

发现这个函数对地址 0x400000
进行了一些操作,我们跳进去看一眼

先看 sub_1E092B
,发现前面用了很长一段代码从PEB表找LDR加载器,再枚举模块找Kernel32,再通过匹配算法找GetProcAddress函数,最后再找到 LoadLibraryA函数。

这里就是在匹配函数名称

最后的返回结果如下

然后进入后面的函数sub_1E003C
开始调用 VirtualProtect
和 VirtualAlloc
,再用 memset 初始化,最后写入数据。




看起来就是这里了,执行完这几个函数,然后再看一眼 0x400000
地址

4D 5A
开头,显然是藏了一个 PE 文件在里面
但是一开始我根据 PE 头用 idapython dump 的文件有问题,最后看了网上的一些教程选择用 OllyDbg 来 dump
不得不说全插件的 OD dump内存确实很好用,直接用 API break 插件断在 VirtualProtect
和 VirtualAlloc
这里,再继续往下调试找到一个大跳转,新的入口点就在这里,用 OllyDump
插件进行 dump

我们将其 dump 成 exe 文件,再用 ida 进行分析
dump.exe
此时再看导入表就能看到很多信息了

查看交叉引用发现SOCKET逻辑

查字符串,还有一些数据库操作

还有很多每个字符后面都跟了一个 \0
的字符串找不到交叉引用,全部显示 unkown,很奇怪

可以打开文本选项把默认值改成宽字符解决

然后再找这些字符串的交叉引用就能找到一些逻辑



