Hi,我是观宇战队的kenant,今天开始为大家分享恶意文件分析系列文章,第一篇我们聊聊脱壳技术的前世今生。
加壳,是一种通过一系列数学运算,将可执行程序文件或动态链接库文件的编码进行改变(目前还有一些加壳软件可以压缩、加密驱动程序),以达到缩小文件体积或加密程序编码的目的。加壳一般是指保护程序资源的方法,主要目的是防止程序被非法纂改或被轻松逆向得到源码。
与加壳相对应的是脱壳,脱壳一般是指除掉程序的保护,将文件恢复到未加壳前状态的一种手段,脱壳后即可修改程序资源,同时也便于破解者进行静态分析,逆向得到程序源码。
脱壳根据脱壳的方式也可以分为硬脱壳和动态脱壳。
**硬脱壳:**顾名思义,该脱壳方法偏向于通过静态分析,得到加壳的加密或压缩过程的算法,然后根据该算法写出逆向的脱壳算法进行脱壳。但很多壳由于具有加密、变形的特点,其每次加壳得到的程序内容有差异,导致硬脱壳存在一定的局限性,对于强度比较高的壳适用性不强。
动态脱壳:由于加壳程序在运行时必然会对程序进行脱壳操作,这样程序才能正确获取资源正确运行。因此可以对程序进行动态调试,寻找程序真实的OEP(源程序的入口点),然后通过dump内存镜像,并将内存镜像重构、修复为标准的可执行文件,从而得到未加壳的可执行程序。与硬脱壳相比,该方式的适用范围更广,适用于对未知的壳以及强度较高的壳进行脱壳操作。
许多读者也许会问,脱壳的目的是什么,或者说脱壳的意义在哪里?
诚然,现阶段进行软件分析,即使不脱壳,也可以动态分析出程序的运行逻辑,甚至通过动态分析直接逆向出程序的源码。既然如此,脱壳的意义在哪里?
加壳会导致程序的资源等被加密或压缩,导致静态分析无法对程序的资源信息如(字符串、IAT(导入地址表)等)利用工具直接进行查看。对于有汉化需求的人来说,资源信息无法直接读取,无法利用资源编辑工具对资源内容进行替换、修改。对于破解或分析软件的人来说,通过脱壳,就可以静态对软件进行进一步分析,通过动静分析结合的方式快速定位关键代码点进行破解。
大家都知道外界有很多加壳对应的程序,如UPX、ASPACK、ASProtect等等,利用加壳程序对可执行程序进行操作后,程序即被加壳处理。
相应的,有加壳程序也必然会有脱壳程序存在,如UPX本身即可对UPX加的壳进行脱壳处理,ASPACK以及ASProtect本身虽没有脱壳的功能,但存在UNASPACK以及ASProtect-Unpack之类的程序能够对相应版本的壳进行脱壳处理。
利用工具脱壳的流程一般如下:
利用查壳工具查询壳的类型以及版本信息
通过互联网寻找对应版本的脱壳工具或利用提前收集的脱壳工具进行脱壳处理
使用工具脱壳完之后再次利用查壳工具查询并结合其他手段确认是否脱壳成功
其中第一步相对较简单,可以利用PEID、ExeinfoPe等常见工具进行壳的甄别,这里可能存在的问题在于某些壳的特征可能还未纳入到相关工具中,导致可能无法识别到某些壳的存在,无法进行后续步骤。可能的解决办法包括自主收集较新的壳的指纹特征,在工具的特征库中写入自行发现的特征来进行壳的识别。
第二步的工作是否有效取决于第一步工作的准确性,如果第一步工作的识别结果出现错误或者未发现有效信息,则第二步的工作无效。通过互联网收集脱壳工具的方式则较多,包括但不限于通过github、搜索引擎以及各大安全论坛进行脱壳工具的搜索,值得提醒的是,搜集到的脱壳工具可能是存在后门的恶意软件,需要读者借助其他手段进行鉴别。
第三步的作用是确认脱壳是否成功,可能存在以下几种情况导致脱壳失败:
查壳工具识别结果错误
搜集到的脱壳程序版本不适配或脱壳程序本身存在问题
壳存在一定的变形导致通用的脱壳程序存在问题
可以看到,工具脱壳存在很大的局限性,如果壳较新或存在变形则可能完全失效,此时就只能考虑通过手工脱壳来进行处理。
由于使用工具脱壳不是本文的主要内容,在此不进行过多赘述。
手工脱壳流程大致如下:
寻找程序OEP
判断是否存在Stolen code,若存在stolen code,则对其进行修复
dump镜像文件
修复IAT
手工脱壳指通过手工调试更进,找到真实的OEP从而进行脱壳,若壳存在Stolen code的现象,则还需在找到了真实的OEP之后,对被stole的代码进行补全处理,然后对程序IAT等内容进行修复。(注:stolen code举个简单的例子就是程序入口点的代码在所加壳的程序解压或解密过程中执行,导致OEP处代码会存在部分缺失,具体参见下图)。
通过以上描述,我们可以知道,手工脱壳的本质就是通过一些手工调试的手段,跳过壳需要执行的代码内容,无限地逼近真实的OEP,同时通过对常见语言入口标志的了解,对被偷取的代码进行恢复处理,最后对加壳程序加密或压缩的IAT进行修复。
寻找OEP的方法:
寻找OEP的方法主要包括以下八种方法:这里先对八种方法进行列举,每种方法的详细内容本文第四节会结合实例进行介绍。
单步调试法
ESP定律法
一步直达法
模拟跟踪法
SFX法
内存映像法
最后一次异常法
利用应用程序调用的第一个API
本节我们通过示例对以上提及的手工脱壳方法都利用程序进行示例演示,并对脱壳时涉及的IAT修复进行一个简单讲解。
通过Ollydbg等调试工具进行单步跟踪,一直跟踪至真实的OEP处,由于单步跟踪时会存在很多跳转以及函数的调用等,如果一条条一直跟进会花费非常多的时间,因此需要通过一些方式跳过循环以及函数调用的内部过程,如遇见向上跳转的指令跳过,call远地址函数跳过,call近处函数跟进等等。
以Aspack V2.12作为示例,首先可以用PEID或exepeinfo查询壳的类型
然后用OD载入对应的加壳文件,进行单步跟踪
程序首先pushad,然后进行一个近call,call 0040D00A,此时eip为0040D001,因此为近call,此处使用F7跟进(如果使用F8,程序直接运行,也就是程序直接跑飞,也可以分析出此处应该F7跟进继续分析)。
然后可以看到OD界面如下:
F8步过retn到jmp,发现是下跳,F8跟进,然后紧接着是call0040D014,近call,F7跟进。
后续根据上面提到的原则,一直F8,遇到向上跳转处,选中跳转的下一条指令F4执行到下一条指令然后继续执行。
后续一直跟进到0x004010CC处,发现此处代码如下,这是由于此处代码之前被解压缩,导致OD可能认为此处的内容是字符串等,未准确识别为指令,可以使用call+A或删除分析恢复。
删除分析后发现入口处代码与VC++的程序相似,尝试使用OD插件进行脱壳,由于不涉及IAT重建,脱壳后即可直接运行,查询发现为VC++6.0程序,脱壳成功。
由于很多压缩壳以及部分加密壳,在执行壳的解压缩或解密过程中,会先将当前寄存器状态压栈,如pushad、pushfd等,此时ESP会发生变化,当解压缩或解密程序执行完毕后,需要将之前压栈的寄存器出栈,由于在解压缩及解密过程中,几乎不可能对此处ESP的值进行其他操作,因此再次对这个ESP地址进行操作时,通常就来到了程序真实OEP的不远处,只需要少许几步单步跟进即可实现脱壳。
同样用Aspack v2.12用做演示
载入程序,发现ESP值为0013FFC4,同时可以发现入口处第一条命令为pushad
此时我们先F8运行pushad,发现ESP值出现变化,值变为0013FFA4
此时可以右键ESP,选择HW break [ESP],或者数据窗口跟随,右键,添加硬件访问断点,然后运行程序,来到下图所示处,此时建议将硬件断点取消掉,避免后续再次访问该地址
然后单步可直接跳转至OEP处,与单步脱壳法一致。
由于很多壳具有这样的特性:通过无条件跳转直接jmp至真实的OEP处,因此可以通过对jmp的机器码E9来找到真实的OEP。同时由于某些壳一开始有pushad命令,对应的在脱壳结束时有popad命令,因此可以直接根据程序开始处的压栈命令寻找对应的出栈命令来查找真实的OEP。
示例壳依然如上,我们知道入口点存在pushad这样的指令,因此其大概率存在popad这样配对的出栈指令用于恢复寄存器环境,载入后直接使用ctrl F进行popad的指令搜索,由于壳中间可能还夹杂着其他的popad,因此可以使用ctrl L寻找下一个popad,直至寻找到我们所需要的那个。
第一个明显不太符合,附近的代码都不满足入口点代码特征
然后我们一直找到下图所示popad,发现其正是入口点前的那个popad。
OD的命令行插件,tc/toc命令分别是跟踪步进以及跟踪步过,直到满足条件就暂停程序,因为OEP通常都在第一个区段中,而壳的代码一般都位于后面的区段中,因此可以根据exepeinfo等查看到的区段信息进行模拟跟踪,如第二个区段的开始地址为0x4F1000,则可以使用tc/toc eip < 0x4F1000让OD对程序进行跟踪,通常可以直接跟踪到OEP。但由于模拟跟踪在每运行完一条指令后都需要判断eip是否满足条件,因此其速度较慢,同时如果壳代码和OEP代码在同一区段中则此方法不适用。
示例环境同上,载入程序,输入alt + m,发现代码段在内存的00401000,大小为4000,因此其代码段的地址均小于00405000,我们使用模拟跟踪法,输入tc eip < 00405000进行跟踪。
跟踪在中间停了,但是发现这里明显不是OEP。
我们F8跟一步,发现eip又大于0x00405000了,我们此时又可以继续使用tc eip < 00405000继续跟进。
跟踪结束后发现直接跳到OEP。
可以发现,如果我们不理解为什么要设置eip < 00405000的话,第一次执行结束,发现没跳到OEP,可能就不知道如何继续开展了,仔细分析就发现只是一条命令位于.text段,跳过后即可使用命令行跳转至OEP。
SFX法与模拟跟踪法类似,只是不需要我们自行去设置条件让分析停下来,而是由OD来决定是否跟踪到了真正的入口处,包含块方式跟踪以及字节方式跟踪,其中块方式跟踪更快,但字节方式跟踪准确性更高。
如图所示,可以再选项,调试选项中,选中SFX,可以点选块方式跟踪得到OEP以及字节方式跟踪OEP,这里我们选择块方式跟踪。
可以发现此处并不是真正的OEP,虽然看起来类似VC++的入口点,我们可以尝试脱壳,但程序都无法正常运行。
然后我们使用字节方式跟踪,就可以得到真正的OEP。
内存映像法是通过对内存中的区段下断点从而找到OEP,由于程序在运行前必然会对运行程序所必要的资源段及代码段进行解密或解压缩,因此壳的一部分代码必然会存在对资源段、代码段的访问或写入请求,因此我们可以依次对程序资源段以及代码段下断点然后跟踪到OEP。
内存映像法的示例用ASProtect V1.23的壳。
与上面相同,使用OD加载程序,程序停在入口点处。
然后可以使用alt + m进入内存界面,对资源段,也即.rsrc段下断点,然后运行。
然后我们继续进入内存界面,对.text段下断点,然后接着运行。
程序来到以下界面,此时再结合其他方法,比如单步跟踪法、模拟跟踪法进一步进行分析,可以得到OEP,这里笔者使用模拟跟踪法进行分析,使用tc eip < 00432000,然后即成功来到OEP。
由于内存映像法是对一个段进行下断点操作,所以很难直接通过两个断点直接跳转到OEP,但通过这个方法可以跳过壳前面对资源段以及代码段解压或解密的大部分操作,当解压完成后,如果eip回到代码段中,基本就来到了OEP。
有的加壳程序会在执行壳代码的时候设置很多异常来干扰脱壳破解者,其会在各个异常的异常处理程序中检测断点以及进行反调试。所以我们如果还用esp定律,内存映像等下断点的方法就会失效。我们需要将这些异常执行完之后在采取以上办法。最后一次异常法就是寻找到程序最后一次异常发生的指令,当最后一次异常执行后我们在采取常规方法寻找OEP。
最后一次异常法的示例使用PEncrypt加壳的程序进行演示。
同样使用OD载入程序,然后取消所有异常
然后使用shift+F9进行运行,测试多少次shift + F9程序正常运行,这是为了跳过程序前面为了反调试而触发的一些异常。经测试,本程序shift+F9两次之后程序即正常运行,因此我们重新载入,shift+F9一次跳过程序反调试触发的异常,然后来到如下界面。
然后关注右下角的SE处理程序地址,可以看到这里的地址是0x4DCCD7,我们ctrl + G,然后对该地址下断点,shift+F9,然后再取消断点,然后我们再单步跟踪或者模拟跟踪,这里笔者尝试模拟跟踪,然后很快跟踪结束来到OEP。
这里很多读者可能会问,为什么要对SE处理程序处的地址下断点然后shift+F9运行,因为如果不在这里下断点然后shift+F9忽略异常运行的话,本程序会陷入异常然后终止,导致我们无法继续分析,因此我们需要shift+F9跳到这个异常处理程序处,然后再继续分析。
通过对应用程序调用的第一个API下断点来到达OEP附近,从而进一步寻找到OEP。例如GetVersion、GetModuleHandle等。需要注意的是,这个API函数的选取有一定的技巧,如果壳中也大量调用了这个API函数,那么这个函数的定位将会非常耗时,因此需要选取的是壳内调用次数较少,同时又在OEP附近常调用的函数。
这里我们选择nspackV1.3的加壳程序作为示例。
首先我们使用OD载入程序,根据前期的知识储备,我们知道入口点附近可能调用的函数可能包括GetVersion,获取操作系统版本,方便进一步根据系统版本进行适配等操作,因此我们使用at GetVersion将代码段跳转到第一个使用GetVersion的地方。
此时我们已经位于GetVersion调用的代码中了,此时需要执行到返回处,返回至调用GetVersion函数处,所以我们在retn语句处下断点,然后运行至该处,之后再取消断点。
然后我们执行F8即可返回至调用该函数的代码处。我们知道这是进入OEP后调用的前几个函数,因此我们需要在此处向上寻找OEP。
然后我们发现VC++的入口点代码特征,判断此处可能为OEP,尝试脱壳进行验证,确定其确为真实OEP。
手工脱壳在找到OEP并dump出镜像文件后,由于程序的IAT在加壳的过程中也会被加密或压缩,因此需要对IAT进行修复,避免函数调用错误。
这里还是以北斗的加壳程序作为示例。这里我们通过先前的分析已经得到了OEP的地址,然后我们就可以使用LordPE等工具dump出目标程序。
然后我们可以通过ImportREC等工具自动修复IAT或者手工定位IAT的地址以及长度来对IAT进行修复。
自动修复如下图所示,只需要输入OEP地址,然后点击自动查找IAT,获取输入表,显示无效函数,将无效函数剪切即可。
如果想要手工定位IAT的偏移地址,则可以使用以下方法。
对于IAT的调用,不同链接器可能使用的方法不同,这里我以VC++6.0作为示例,VC++6.0调用IAT是使用FF 15来进行调用的,因此我们可以在OEP附近搜索二进制字符串FF 15,来找到IAT调用的地址,如下图所示:
然后可以看到他调用的地址为0x004422A0,然后我们可以在命令行对该内存地址进行跟进查看,输入dd 0x004422A0。
可以看到左下角有很多函数地址。我们先向上,找到第一个不是函数地址的地方。
由于0x00442000以上都是空的,说明IAT的起始地址为0x00442000,然后我们再向下寻找IAT结束的地址,得到IAT的大小。
通过向下寻找,发现结束地址为0x00442654,因此IAT长度为0x654,与ImportREC找出来的一致(ImportREC存在一个无效指针,因此虽然写的长度为0x65C,实际剪切指针后长度为0x654)。
同样也可以通过右键->查找->所有模块间的调用,显示出调用函数列表。
然后我们双击其中某个程序的函数(图中框选的即为程序的函数),不要点击系统函数。
此时来到函数调用处
然后可以按回车进入函数调用过程内部。可以看到函数内部会存在对IAT表中函数的调用。
然后寻找IAT表地址的过程与方法则与前面所述一致,到此两种IAT修复方法介绍完毕。
本文主要介绍了手工脱壳的八种方法,包括单步跟踪法、ESP定律法等,可以发现其实每一种方法都有他自身的优势,也存在一些不足。通过对示例的脱壳过程可以发现,对一个程序进行脱壳的时候,也不必局限于使用一种方法去脱壳,很多时候将多种方法结合可以使脱壳变得简单很多。例如,单步跟踪法是一种最基础的分析法,对于比较复杂的加壳程序,几乎都需要单步跟踪法与其他方法相结合来分析。而ESP定律法则适用于一些特殊的壳,比如使用了堆栈来保存寄存器环境的壳。
同时脱壳时最重要的一点是要熟悉各种编译器,各种语言的入口点特征,否则即使通过脱壳的方法跟踪到了真实的OEP,可能也不知道已经可以脱壳了。脱壳作为逆向过程中的一部分,也需要多练习,积累经验,熟能生巧,希望各位读者也能早日达到自己想要达到的高度。
文章来源:freebuf.com
黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
如侵权请私聊我们删文
END
多一个点在看多一条小鱼干