某病毒样本分析报告

一些碎碎念

学了这么久逆向感觉一直面向的CTF题,很少接触真正的实际应用,刚接触样本分析感觉很多都不会,找一篇写的不错的文章入个门先

[原创]样本分析核心手法:一个简单的开始-软件逆向-看雪-安全社区|安全招聘|kanxue.com

哈哈由于暑假比较摆烂,中间再插播一个工业互联网决赛,所以学的很慢,调的也比较慢,结合一些网上的文章还有一些样本分析实例勉强做了一些浅薄的分析,过程如下:

样本名称:8aca8fe2dfa143a3210f00df3f43d4185fefa7a3

分析软件:火绒剑/idapro/OllyDBG/studyPE/010Editor/exeinfo

调试环境:Windows 10 x64 22H2

行为分析

这里使用火绒剑进行行为分析

发现有很多注册表操作,对文件目录的读取和修改行为,以及网络连接行为,连接的 ip 地址经查询在保加利亚,最后还把自己给删了

image-20240731234706504

image-20240801000114562

image-20240801000539666

查壳

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

image-20240731234449548

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

image-20240801000736952

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

image-20240801000947478

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

image-20240801001534818

看来是藏了东西的,我们先动态调试一下。

动态调试

WinMain开始调,里面只有一个函数

看起来很大一坨,但是有用的只有几行

image-20240801002405601

image-20240801002421786

image-20240801002434834

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

image-20240801002529393

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; // eax
int v2; // [esp+10h] [ebp-34h]
_DWORD v3[8]; // [esp+14h] [ebp-30h] BYREF
int v4; // [esp+34h] [ebp-10h]
int v5; // [esp+38h] [ebp-Ch]
int v6; // [esp+3Ch] [ebp-8h]
int v7; // [esp+40h] [ebp-4h]

*(_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里面的很多函数的地址存到了一个表里

image-20240801003148116

我们恢复成结构体,恢复完以后代码就很清晰了,再查一下 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; // eax
int v2; // [esp+10h] [ebp-34h]
_DWORD v3[8]; // [esp+14h] [ebp-30h] BYREF
int v4; // [esp+34h] [ebp-10h]
int v5; // [esp+38h] [ebp-Ch]
void *v6; // [esp+3Ch] [ebp-8h]
int v7; // [esp+40h] [ebp-4h]

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; // ebx
int v2; // edi
int v4[137]; // [esp+Ch] [ebp-224h] BYREF

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函数,前面两个函数不知道在干嘛,像是在对一堆数据做解密或者压缩操作,我们接着往后面调

image-20240801004629895

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

image-20240801004723687

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

image-20240801005048817

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

QQ20240730-232806

这里就是在匹配函数名称

image-20240801005529852

最后的返回结果如下

image-20240801005926807

然后进入后面的函数sub_1E003C

开始调用 VirtualProtectVirtualAlloc,再用 memset 初始化,最后写入数据。

image-20240801010656731

image-20240801011228263

image-20240801011302073

image-20240801011323762

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

image-20240801011534257

4D 5A 开头,显然是藏了一个 PE 文件在里面

但是一开始我根据 PE 头用 idapython dump 的文件有问题,最后看了网上的一些教程选择用 OllyDbg 来 dump

不得不说全插件的 OD dump内存确实很好用,直接用 API break 插件断在 VirtualProtectVirtualAlloc 这里,再继续往下调试找到一个大跳转,新的入口点就在这里,用 OllyDump 插件进行 dump

image-20240801013023981

我们将其 dump 成 exe 文件,再用 ida 进行分析

dump.exe

此时再看导入表就能看到很多信息了

image-20240801013313796

查看交叉引用发现SOCKET逻辑

image-20240801013451898

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

image-20240801013613155

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

image-20240801013804570

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

image-20240801015101445

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

image-20240801020146644

image-20240801020122391

image-20240801020201972

image-20240801020214824