本帖最后由 Hyabcd 于 2016-2-19 17:21 编辑
一、前言 代码隐藏是病毒木马免杀的关键部分。许多病毒木马通过代码隐藏技术逃过杀毒软件的扫描,其中最为常见的代码隐藏方式就是加壳。通过加壳可以对主要代码进行压缩或加密,对一些关键部分进行重定向或者间接调用,本文以一最近出现的国外的恶意软件为例,分析其代码隐藏技术。
二、代码隐藏的大体方法及结构
通过对该恶意软件的逆向分析可以发现,它所使用的代码隐藏方法稍微有点复杂,不过所用到的技术并非罕见。该软件通过多次申请空间进行代码转移,以及对原代码进行重写,对数据进行解压来得到恶意代码内容;除此之外,程序对api的调用进行优化,所有api均为动态调用,恶意代码部分没有任何静态调用的api存在,这对静态分析造成极大的不便。代码隐藏的大体结构如下图所示。
三、具体分析过程
(1)代码隐藏重写部分
首先,程序调用getprocessheap获取进程堆空间的句柄,本程序为00150000,代码如下所示。
[Asm] 纯文本查看 复制代码 00405FD0 |. C78424 F42F00>mov dword ptr ss:[esp+0x2FF4],0x0
00405FDB |. C78424 F02F00>mov dword ptr ss:[esp+0x2FF0],0x3E31
00405FE6 |. 8BB424 902F00>mov esi,dword ptr ss:[esp+0x2F90]
00405FED |. C78424 E82F00>mov dword ptr ss:[esp+0x2FE8],0xFFFF8DD6
00405FF8 |. 88FB mov bl,bh
00405FFA |. 80E3 F2 and bl,0xF2
00405FFD |. BF DA44CA40 mov edi,0x40CA44DA
00406002 |. 2BBC24 E02F00>sub edi,dword ptr ss:[esp+0x2FE0]
00406009 |. 889C24 FF2F00>mov byte ptr ss:[esp+0x2FFF],bl
00406010 |. 894C24 08 mov dword ptr ss:[esp+0x8],ecx
00406014 |. 895424 04 mov dword ptr ss:[esp+0x4],edx
00406018 |. 893424 mov dword ptr ss:[esp],esi
0040601B |. FFD0 call eax ; ntdll.RtlAllocateHeap ;获取进程堆空间句柄
接着程序借助栈空间进行数据的搬移,旨在获取压缩后的代码。具体顺序如下所示。
程序先从00412000开始(该地址位于数据段中)获取加密后的数据置于edi。如图所示。
然后取12ad38中的数据作为计数值传入eax,因此eax作为计数器使用,除外,eax也作为解密用的一部分数据。如图所示。
接着将eax除以固定地址409cf8中的数据,该数据为0x0b,商存储在eax,余数存储在edx。
然后将[esi+edx+0x409ce1]中的值传入edx并进行位数的扩展,esi中存储的是409cfc中值,也为0x0b,也就是说,将余数和0x0b之和作为存储单元和0x409ce1的偏移,并将存储单元中的值传入edx,edx中的值将在接下去的解密中有用处。如图所示。
然后将edi(第一步得到的值)赋给eax,将eax减去edx,得到的值取低八位也就是al寄存器中的值传入0012ad26,该地址作为中转站负责数据的转移。
从以上步骤可以看出,获得所需数据的方法是从地址00412000开始获得数值1,从0开始对0x0b取余,得到的值加0x0b和0x409ce1作为地址获取数值2,数值1减数值2得到的值取低八位即为所需的数值。
除此之外,程序对eax进行递增的方式也比较特别,不是直接对eax加1,而是将0x31bf和存放数值0x31be的地址中的值异或得到1再加上原始的值,如图所示。不知是有意为之还是程序自身特点。
获取数值后将数值传入154038开始的地址,此处为堆的头部,有明显的头部特征,由此循环,数据就被传递到堆中。如图所示。
数据传输完毕后的堆空间如图所示。
可以看出,该段内容有很明显的pe文件的特征。
接着,程序调用RtlAllocateHeap分配一段堆内存,然后调用RtlDecompressBuffer将之前的内容解压到该段堆内存中。如下所示。
[Asm] 纯文本查看 复制代码 0040651D |. 895C24 14 mov dword ptr ss:[esp+0x14],ebx
00406521 |. 894424 10 mov dword ptr ss:[esp+0x10],eax
00406525 |. 894C24 0C mov dword ptr ss:[esp+0xC],ecx ;之前的堆内存
00406529 |. 897C24 08 mov dword ptr ss:[esp+0x8],edi
0040652D |. 897424 04 mov dword ptr ss:[esp+0x4],esi ;分配的堆内存
00406531 |. C70424 020100>mov dword ptr ss:[esp],0x102
00406538 |. FFD2 call edx ; ntdll.RtlDecompressBuffer
如下图所示,解压后的文件为一个正常的pe文件。
接着程序申请两个内存,在本次测试中卫3c0000和3d0000(不固定)。然后将堆内存中的代码转移到3d0000中。
接着将408800起始的代码转移到3c0000中,408800处于代码段中,作者不直接执行该段代码而是转移到动态分配的内存中也是为了通过动态的执行代码进行免杀,不过在程序开头中有jmp 408800的语句,可能是作者为了防范内存分配失败而准备的备用方案。
接着程序进入3c0000开始执行,在该段代码中,程序将原本pe文件的所有内容全部清空,然后写入3d0000中的代码,来了一次代码的完全替换,将一个pe文件变为另一个pe文件,而作者把408800中的代码放入动态分配的内存中执行也是为了后续删除代码而做的准备。
至此,程序真正的代码才出现。可以看出,代码经过一层层的隐藏,最后经过一层层的剥离才出现,其中利用到堆空间,栈空间进行暂存。
(2)api隐藏部分
进入主程序后可以发现,所有api调用都以call eax的形式出现,而在call eax之前都有一个call 40e644,该函数的返回值及所要调用的api的地址。如图所示。
40e644中会判断该api是否已经获取地址,如果未获取,程序将动态获取api地址。
接着程序通过数据段获取所需要的库名称,例如“kernel32.dll"。然后利用PEB(进程环境块)获取api地址。代码如下。
[Asm] 纯文本查看 复制代码 0040EA0D 64:A1 30000000 mov eax,dword ptr fs:[0x30]
0040EA13 8B40 0C mov eax,dword ptr ds:[eax+0xC]
0040EA16 8B48 0C mov ecx,dword ptr ds:[eax+0xC]
0040EA19 8BC1 mov eax,ecx
0040EA1B 8B00 mov eax,dword ptr ds:[eax]
0040EA1D 8B58 18 mov ebx,dword ptr ds:[eax+0x18]
fs段存储的是TEB的内容,而fs:[30],也就是TEB偏移0x30的位置存储的是PEB的指针,而PEB偏移0xc存储的是PEB_LDR_DATA结构,Ldr里面存有加载的dll的信息,而Ldr偏移0xc存放的是InLoadOrderModuleList链表,该链表是以进程基址为起始,存放加载的所有模块信息的双向链表,链表的起始存放的是下一链起始的指针,因此作者略去了链表第一链内容的检索,因为第一链对应的是进程基址,而从第二链开始获取。而链偏移0x18位置存放的就是模块基址,例如”kernel32.dll"的基址,作者就通过这样的方式获得所需的dll的基址。更多内容可以查看小小的心所写的LDR链调试手记http://bbs.pediy.com/showthread.php?t=149527
而在链偏移0x30的位置存放着所调用模块名称的指针,作者就是通过该指针中的字符与所要求的字符相比较从而确定是否为自己所要使用到的dll。在这里可以发现,存有ntdll.dll的名称的指针位于ntdll的空间内,而其他的则存放在InLoadOrderModuleList结构体中。
获取了所调用的dll基址后,程序从数据区获取加密后的数据并进行异或运算获得函数名,在获得函数名的过程中,程序进行多次异或,分段获得函数名称,然后调用GetProcAddress获得所用函数地址,和一般动态调用不同,该种方法省去了LoadLibrary的部分,也躲过了一些杀软基于api检测的监控。
而如何获取GetProcAddress函数的基址呢?作者用同样的方式获得kernel32.dll的基址,然后遍历该dll中的所有函数,和GetProcAddress名称进行比对,比对成功则获取函数基址。
到此,api动态获取的方式已经显而易见,可以说虽然使用的方法并非新颖,但也是别有用心。
|