0x00 前言
熟练的阅读汇编代码是恶意软件分析时的基本功。
在本节中,我将以一个比较简单的Downloader(下载者)程序为例,以纯汇编的角度来对该恶意样本进行分析。
本节中所使用到的样本已经上传到了app.any.run
可以访问这个地址进行下载:https://app.any.run/tasks/ba68e2aa-2083-48ad-ac3f-34dcc9e4446b
这个地址是该样本在app.any.run上面的沙箱页面,访问该页面,然后单击Get sample即可下载
下载的样本解压密码为:infected
0x01 所需工具
由于是纯静态分析,这里只需要使用 IDA7.0
0x02 反汇编测试
编译流程
在开始之前,先对编译和反汇编有大概的了解。
首先,编译的时候,以C语言为例,一个程序在编译时会经过如下四个阶段:
- 预处理
- 编译
- 汇编
- 链接
在预处理阶段,编译器会将程序中所有使用到的预处理指令进行替换,比如#define #inclde<stdio.h> 这种都是预处理指令,一般来说 在C语言中,带#的,就是预处理指令。预处理指令是程序编译时指定的第一个操作。此外,在该阶段,编译器会删除掉源代码中所有的注释。
在编译阶段,编译器会将源代码翻译成一个包含汇编代码的文件。也就是将高级语言翻译成汇编语言。
在汇编阶段,编译器会将刚才翻译好的汇编文件转换为机器语言指令,即全由0和1组成的指令。
在链接阶段,链接器将会处理合并代码,生成一个可执行文件,也就是最后的exe文件。
我们拿到的exe文件本质是一个二进制文件,就是0和1组成的字节流,但如果全部用0和1来显示的话,就没有可读性,所以通常情况下都以16进制来显示。
我们将exe文件放入十六进制编辑器(如winhex、010Editor等)中可以看到如下的数据
反编译概要
当IDA加载一个exe文件时,本质就是加载这个二进制流。
IDA拿到之后,首先识别到这是一个exe文件,然后把对应的十六进制代码再根据语法转换为汇编代码。
最后将这些汇编代码根据语法转换成C语言伪码。
VC6.0测试
首先以VC6.0为例,看看一个程序反汇编之后的样子。
我们在VC的编译器中写如下的代码然后生成对应的exe文件
可以看到,Debug版本生成之后,仅仅6行代码,却生成了169k的exe文件。
然后在IDA中加载该程序
main函数如下:
这里调用了一个main_0(),双击跟进去:
除了这里没有#include<stdio.h>
其余部分与vc里面写的几乎一模一样,没有#inclde<stdio.h>的原因之前已经讲过了,这部分属于预处理指令,已经在编译之前就处理掉了,所以反汇编之后,不会再显示。
接着来看一下汇编代码:
可以看到,关键的代码就只有框起来的部分,就是push了一个字符串,然后调用print函数进行输出,至于其他部分代码是干啥的,我们后面再讲。
此外,VC编译的时候,会生成大量的库函数,所以反汇编出来,也会生成大量的库代码,在本程序中,我们只在main函数中进行了输出,但是程序却生成了这么多函数:
所以在分析的时候,需要格外注意,很多代码都是编译器生成的库代码,不要分析到这些代码去了,不然会很浪费时间。
0x02 具体过程
通常来说,使用IDA可以将汇编代码转换为C/C++的伪代码。但是为了加深汇编的理解,在本样本中,将完全从汇编的角度来进行分析。
在本节最后,也会贴上伪代码分析的部分,如果觉得汇编分析太过抽象,可以先跳到最后看伪代码分析的部分,理解样本的功能、每一步在做什么之后,再回头过来看汇编,应该会更好理解。
首先将样本拖动到IDA中,IDA自动识别这是一个PE文件
直接单击OK进入下一步
在弹出的选项中都单击OK,最后成功加载后默认显示如下
导入表分析
通过IDA加载样本之后,默认会停在样本的入口点,即如果程序没有做特殊的处理,IDA默认停留的地方,就是程序运行时的入口点。此时不要急着直接分析代码,可以先通过一些其他信息来对该样本进行一个初步的判断。
首先是导入表分析,在函数未加壳的情况下,导入表中会包含程序所使用到的一些系统API,通过这些API,我们通常可以对样本的基本行为有个大概的了解。
默认情况下,导入表会自动打开
如果不小心关掉了,可以通过ALT + 6 的快捷键重新打开。
通过导入表,可以看到在本程序中一共使用了39个导入函数。我们可以对这些导入函数进行初步分析。
Windows的设计比较人性化,WindowsAPI的命名也倾向于与函数的功能相关联,所以通过导入函数的命名通常便可以推断这些函数的大概功能是什么,不过想要了解详细信息如参数、返回值等,还是要查询手册。
这里的WriteFile函数很明显是写入数据到文件。
CreateFileA用于创建文件。
CreateThread用于创建线程。
….
最下面的htons、send、connect我们可以猜测这些函数与网络请求相关。
如果想要知道这些函数的具体用法,可以直接在搜索引擎中搜索该函数。
这里第一个就是Windows的官方手册,第二个应该是别人博客写的代码示例,我们在使用的过程中可以先看看官方文档,然后再看别人博客的使用方法加深印象。
现在通过对导入表的初步分析,我们大概可以推测该样本有可能会在本地创建文件写入数据、有可能会进行网络请求(通过send发送数据)、有可能会创建新的线程。接下来,我们可以结合这些信息对字符串表进行分析。
至于为什么是可能呢,我们继续看一下用于测试写的demo1.exe的导入表信息:
可以看到,在该程序中,导入表比分析的真实样本还要更多,其中也包含了一些比较奇怪的API,所以在分析导入表的时候,只能对程序起到一个初步分析的作用,让我们心里明白 程序大概会执行哪些操作。
如果实在想搞清楚这些API在哪里使用了,用来干什么了,我们可以通过IDA的交叉引用来确定。
就以我们分析的这个恶意样本为例,我们在样本的导入表中看到了一系列网络请求相关的API。
比如我们想要查看gethostbyname的调用位置,我们可以双击该API的名称:
接着鼠标单击框起来的地方,然后按X,弹出的对话框中就会显示所有用到了这个函数的地方
单击OK,跟进过去:
可以看到,这里是在一个名为gethostbyname的函数中调用了,我们继续对gethostbyname按X查看交叉引用,过来之后,gethostbyname函数前后的代码如下。
我们往上找找,看看是在哪个函数中,看到这样的汇编代码,我们即可知道回到了函数的头部,函数名为sub_401068
继续对sub_401068进行交叉引用:
然后对StartAddress进行交叉引用,然后找到函数头部,发现是sub_4013A3
继续对sub_4013A3进行交叉引用,回到了sub_401499函数中
对sub_401499函数进行交叉引用,回到了start函数这里,就是我们最开始看到的IDA默认停留的入口点。所以我们现在通过交叉引用搞清楚了gethostbyname这个API的调用链,也基本确定了这个函数是由用户代码编写调用的,以及函数的调用位置。
一个反面例子,我们刚才分析自定义的demo1.exe发现,在只执行了printf函数的情况下,导入表中有WriteFile函数,我们可以对该函数进行交叉引用试试。
同样的方法,我们对WriteFile进行交叉引用,可以看到调用处有点多,这里需要注意一下Address标题下的内容,这里全是以__开头,基本可以确定是系统的代码调用,我们随便找一个地址过去。
然后一层层的往上找,最后找到了_printf函数
然后再往上找,找到了在main函数中的调用。
经过分析,我们最后可以发现,WriteFile在Printf函数的底层实现中进行调用。与用户代码无关。
所以,有很多API是正常程序、恶意程序都会使用到的,我们不能看到程序的导入表中有某个API,就说程序一定有某个功能。
字符串分析
字符串表的快捷键是shift + f12
字符串表中会显示出IDA对该样本提取的所有字符串。在程序未加壳或是混淆的情况下,我们可以通过Strngs Window 查看程序中所用到的字符串,在该样本中如下:
这里字符串很少,但其中却包含了一些比较关键的信息。
比如我们可以看到URL地址:dload.ipbill.com
以及关于该地址的一个路径:http://dload.ipbill.com/del/cmb_211826.exe
结合HTTP/1.0 rnrn、GET 等信息,结合之前对导入表的分析,我们基本可以推测该样本会请求我们这里看到的地址,然后CreateFile创建一个文件,通过WriteFile将请求的数据写入到文件中。最后CreateThread执行该文件。
同样的,字符串也可以进行交叉引用,我们双击想要查看的字符串:
然后选中前面的这个变量名,按X
然后对这里的off_403004进行交叉引用
可以回到StartAddress中,与我们之前分析的gethostbyname调用位置差的不远。
代码分析
经过导入表和字符串表的分析,现在对程序的基本功能有了一个概要的猜想了,接下来看看具体的代码。
回到IDA的代码窗口,查看一下star函数
start函数非常短。
首先通过xor eax eax的方式将寄存器eax的值清零。
接着push了四个eax(0)入栈,然后通过call指令调用sub_401499函数。
sub_401499函数调用完成之后,再通过ExitProcess函数结束进程。
我们跟进到sub_401499函数中,函数内容如下,我们一点点分析它
首先是三条汇编指令
push ebp
mov ebp esp
sub esp,1Ch
这三条汇编指令用于开辟当前函数的栈空间。
关于栈空间,讲一下原理。
就是函数调用的时候,是基于栈的。
系统在调用函数的时候,首先会将函数的参数从右往左的方式依次入栈(C语言)。
最后入栈的是函数的返回地址,这个地址很重要,关系着函数执行完成之后该回到哪里继续执行。
比如我有个函数fun1(int a, char b, int c )
我在main函数中调用了fun1函数,
那么参数入栈的时候
最先入栈的是变量c
其次入栈的是变量b
接着入栈的是变量a
最后入栈的是main函数调用这个fun1函数之后的地址。
以刚才我们加载的样本为例,入口点的伪代码如下所示:
按下tab,转换成汇编代码:
可以看到,程序首先是通过xor eax,eax的操作将eax赋值为0
然后四个push 分别将0入栈,最后通过call sub_401499这里。从反汇编代码中,可以看到并没有将返回地址入栈的操作,程序其实是通过call执行此操作的。
call指令我们可以理解为函数调用,call指令后面会跟一个地址,这里是sub_401499表示call会跳转到401499这个地址,而这个地址被IDA识别出来了是一个函数的开头,所以会自动加上sub标志。
同时,call指令我们可以分解为两个指令
即
push xxxx
jmp xxxx
在这里,是push 0040150d 然后jmp 00401499
0040150d是下一条指令的地址,00401499就是将要跳转过去执行的地址。
我们可以在调试器中看看,首先od加载我们的代码,默认如下:
代码区的部分与我们在IDA中看到的部分一致,此时我们可以从寄存器区看一下EBP的值,然后在下面的堆栈区找到ebp的地址。
我们首先点一下堆栈区,然后ctrl + g 弹出窗口,在输入框中输入寄存器的名称或者地址然后跳转过去
我们可以看到,由于此时程序还没有运行,EBP(0012FFF0)的值为0
然后我们再看一下ESP,ESP的值是0012FFC4,所以当前函数(start)函数的栈大小如下所示:
我们鼠标点回到代码区,然后F8单步运行一下。
eax清零,esp和ebp不变
继续F8 执行第一个push eax
ebp不变,esp-4得到了0012FFC0
因为push操作入栈,所以栈顶esp往上抬了,又因为入栈的参数是int类型,在32位操作系统中,一个int占四个字节,所以这里push eax之后,esp的值减少了四。
同理,我们继续F8往下走三步,停在call 指令处,此时esp=0012FFB4
此时F7进入到call中
可以看到,我们只是执行了一个call 指令,但是esp的值依旧被减少了4,我们看看此时esp的内容:
如图所示,此时esp存放的是一个地址,0040150d,就是我们之前看到的,call指令之后的地址。
此时EBP还是跟我们之前看到的一样,是0012FFF0
讲了这么多,现在终于可以来讲我们最开始在IDA里面看到的这个函数开始的三条汇编指令了。
函数最开始有如下的指令:
push ebp
mov ebp,esp
sub esp,1Ch
我们已经分析到,此时ebp存放的是在start函数中的栈底0012FFF0
现在push ebp,可以看到,push之后,esp的值变成了0012FFAC
我们查看esp,可以看到该地址中存放了一个地址。这个地址就是上个函数的ebp值。
接着程序会执行mov ebp,esp的指令。
此条指令执行之后,会将esp的值移动到ebp中。
执行完之后,esp和ebp就指向了同一个地方:
最后通过sub esp,0x1c的操作,将esp减去0x1c 也就是向上移动0x1c个大小的空间
这个0x1c 就是当前栈的大小
我们可以看到,此时esp和ebp已经重新赋值。
且现在ebp的地址存放的是上一个函数ebp的地址,ebp后面的地址存放的是返回地址。
这样,这个函数执行完之后,就可以通过当前ebp的值,找到上一个函数ebp的地址,然后根据当前ebp之后的那个地址,返回到上一个函数继续执行。
现在我们知道函数调用是,栈区的变化了,我们回到IDA中看代码。
首先,push edi,将edi入栈,保存edi的值
后面xor edi edi 将当前edi的值清零
然后cmp指令比较edi和[ebp+arg_4]的值
这里ebp + arg_4 的操作是取参数(根据Windows的内存生长方向,程序一般通过ebp+xx取参数,ebp-xx取局部变量)。
比较之后,如果edi 和 [ebp+arg_4]不相等,则执行后面的jnz操作,跳转到loc_4014B4的地方。
我们知道,push进来的四个参数都等于0,且edi通过xor的操作之后也等于0,所以这里不会执行jnz操作,会继续往下执行。
接着,程序push了另外一个参数,然后call进入到了sub_401437函数中:
我们双击sub_401437进入到该函数,函数整体内容如下:
在函数最开始,还是开辟栈空间的操作,这里是开辟了0x28大小的栈空间。
且在这里我们可以看到一些红色的字体,这些字体就是系统的API,我们可以分别查一下这些API是干什么用的。
通过这些API 我们基本可以确定当前函数是用于程序初始化的,注册窗口,用于后面显示弹窗
那我们就可以返回去接着往后看了,按下esc返回到刚才的函数:
在sub_401437函数调用完成之后,程序会通过test eax ,eax的操作判断eax的值是否为零。
这里判断eax 的值的原因是,当函数有返回值的情况下,返回值默认会存放到eax中。
这里判断eax是否为0,如果等于0,则通过jz跳转到loc_4014C5这个地方,否则继续往下执行。
通过上面的分析我们可以知道,既然sub_401437是用于初始化窗体,那么返回0应该是初始化失败
从代码这里也可以看到,如果eax等于0,跳转到loc_4014C5之后会jmp跳转到loc_4014FD,然后执行retn 指令,程序结束。
所以很明显,这里的call sub_4013A3是另一个关键操作,我们跟进进去
该函数初始化栈完成之后首先会push一些参数,并调用SystemParametersInfoA
通过查询,我们可以得知该函数是易语言中用于操作桌面的函数
接着,程序执行一系列操作对参数进行处理,然后调用CreateWindowExA函数创建窗口
如果创建成功,则跳转到loc_40140E处执行,否则则跳转到loc_401434结束程序
loc_40140E这里的主要操作是通过CreateThread函数创建了一个新的线程
通过查阅CreateThread函数,我们可以知道该函数的关键调用点在参数3,即新线程的起始地址
在当前函数中,参数3是StartAddress 我们双击过去看看
StartAddress如下所示:
在该函数中,程序首先push 了几个参数,然后调用call sub_401068函数
DA已经帮我们识别了参数的类型并标注在后面,我们可以分别查看一下
offset dword_4033E0
off_403004的值
offset dword_4033E0,大小200000的char数组
off_403004是一个下载地址:
那我们现在基本可以猜到,sub_401068这个函数是访问后面这个下载链接,然后将文件下载的,上面那个大小为200000的char数组是用于存放下载的文件的字节流。
我们跟进到sub_401068函数验证一下,可以看到,的确是进行网络请求的一些函数。
包括GET请求,然后将数据读取到char数组中
所以我们回到刚才那个函数,sub_401068调用之后,如果下载失败,则提示Invalid HTTP response,然后调用sub_40102D结束进程
如果请求成功,则会通过jl跳转到loc_40121E处继续执行
在这里,程序首先会创建C:dialler.exe
然后通过WriteFile 将下载回来的数据写入到文件中,最后通过ShellExecute函数执行下载回来的文件。
然后通过ExitProcess退出进程。
整个程序的运行完全结束。
所以梳理一下程序的运行流程
1 判断是否成功初始化窗体
2 判断是否可以成功创建窗口
3 判断是否可以进行网络请求
4 下载文件到本地
5 执行下载的文件,结束进程。
可以看到,该样本的主要功能其实就是通过制定的链接下载程序到本地执行,这类的恶意样本称之为下载者。
0x03 C伪代码分析
其实此样本如果直接转成C语言伪代码,就非常简单:
首先是start函数
在start函数中,程序调用了sub_401499这个函数,当sub_401499执行完成之后,调用ExitProcess函数结束进程。
我们双击跟进到sub_401499函数:
在sub_401499函数中,首先通过三个条件判断,以确保样本运行环境符合预期。
第一个条件是 !a2 也就是在判断a2的值是否为0,a2可以看到是第二个参数,我们在start函数里可以看到,在调用sub_401499的时候,四个参数都为0,所以这里的a2为0,第一个条件通过。
第二个条件是判断sub_401437的返回值是否为0,或者判断sub_4013A3的返回值是否为0
根据||的运算法则,如果第一个条件满足的话,那么后面的表达式将不再计算,我们这里可以先看看第一个函数sub_401437的功能。
sub_401437函数的功能如下:
在sub_401437函数中,首先定义了一个名为WndClass的结构体,类型为WNDCLASSA,并且在后面,通过WndClass.xxxx=xx的方式给这个结构体的成员变量进行赋值。
WNDCLASSA结构体用于存储窗口信息。所以可以知道,该函数的功能为初始化窗体。
并且成功初始化之后,会返回1。
所以当sub_401437函数执行完成之后,将会继续执行sub_4013A3。
我们跟进到sub_4013A3函数:
可以看到,在sub_4013A3函数中,程序首先调用了一个API:SystemParametersInfoA,通过查阅该API我们可以知道,该API用于设置系统参数,大多用来设置桌面、菜单栏等信息。
结合后面的CreateWindows函数,可以推测出这里用于创建并显示窗口。
CreateWindowsExA的返回值会存放到V1中,如果V1等于0,说明创建失败,程序将会终止运行。
如果窗体创建成功,则会通过ShowWindow显示窗体。
接着程序会调用CreateThread,通过查询,我们可以知道该API用于创建一个新进程,其中参数3是新进程的起始地址。
这就说明,当CreateThread调用之后,程序会跳转到该函数第三个参数的位置执行。这里是StartAddress
我们跟进到StartAddress中:
在StartAddress中,首先会调用sub_401068函数,该函数有4个参数,分别是
cp
off_403004
dword_4033E
&v3
其中v3的值是20000
我们可以双击到cp,查看cp参数的值:
这里可以看到cp的值就是dload.ipbill.com这个域名
接着我们查看参数2,off_403004的值:
参数2,off_403004的值是该域名下具体的下载地址
参数3,dword_4033E:
参数3 是一个大小为200000数组的起始地址
所以到这里,我们就算不进入到sub_401068函数,都可以推测该函数会访问参数2的下载地址,将数据读取到参数3位置的数组中,且参数4就是参数3所指向的数组的长度。
我们还是进入到sub_401068函数看一看,与我们推算的一致,程序首先会建立一个socket,然后通过gethostbyname获取cp的地址,然后后面通过connect连接,最后通过send发送数据到服务器,通过recv接受返回值。
现在回到StartAddress中,我们可以看到,dword_4033E0将会作为参数传递到WriteFile函数中。
写入完成之后,最后通过ShellExecuteA执行该文件。
至此,一个downloader的完整功能就分析完成了。
该样本的功能就是:访问指定的地址,下载文件到本地,然后执行该文件。
既然C语言伪码这么简单,为什么还要查看汇编代码呢。
是因为一方面,我们在动态调试的时候,会在调试器中遇到很多的汇编代码,如果我们只会看C语言伪代码,而不会看汇编代码的话,调试的时候将会遇到很多问题。
另一方面,有很多样本都会运行时动态解密数据继续执行,这种时候,在IDA中是无法看到要执行的代码的,我们只能在调试器中跟着走,此时如果对汇编代码不熟悉,不了解各种跳转、调用、赋值、循环操作。会增加很多的分析成本。所以可以在分析的时候多看汇编代码,积累经验。
发表评论
您还未登录,请先登录。
登录