第三十二章-OEP寻踪 在上一章中我们提到了OEP(Original EntryPoint)的概念,也就是应用程序原本要执行的第一行代码,OEP %99的情况位于第一个区段中(本章中有一个例子,OEP就不在第一个区段,我是特意举的这个例子,嘿嘿)。 我们知道当到达OEP后,各个区段在内存中的分布跟原始程序很接近,这个时候我们就可以尝试将其转储到(dump)文件中,完成程序的重建工作(PS:脱壳)。 通常脱壳的基本步骤如下: 1:寻找OEP 2:转储(PS:传说中的dump) 3:修复IAT(修复导入表) 4:检查目标程序是否存在AntiDump等阻止程序被转储的保护措施,并尝试修复这些问题。 以上是脱壳的经典步骤,可能具体到不同的壳的话会有细微的差别。本章我们主要介绍定位OEP的方法。 很多时候我们遇到的壳会想方设法的隐藏原程序的OEP,要定位OEP的话就需要我们尝试各种各样的方法了。 首先我们来看看上一章CRACKME UPX,然后再来看其他的壳。 1)搜索JMP或者CALL指令的机器码(即一步直达法,只适用于少数壳,包括UPX,ASPACK壳) 对于一些简单的壳可以用这种方式来定位OEP,但是对于像AsProtect这类强壳(PS:AsProtect在04年算是强壳了,嘿嘿)就不适用了,我们可以直接搜索长跳转JMP(0E9)或者CALL(0E8)这类长转移的机器码,一般情况下(理想情况)壳在解密完原程序各个区段以后,需要一个长JMP或者CALL跳转到原程序代码段中的OEP处开始执行原程序代码。 下面我们将上一章中加了UPX壳的那个CrueHead的CrackMe加载到OD中: 这里我们按CTRL+B组合键搜索一下JMP的机器码E9,看看有没有这样一个JMP跳转到原程序的代码段。 多按几次CTRL+L。 搜索到了几处,但都不是跳往第一个区段的,直到定位到如下位置: 这个JMP是跳转到第一个区段的,我们在这条指令处设置一个断点,断在这里时,我们按F7键就可以单步跳转到OEP处。 除了搜索JMP指令的机器码以外,大家还可以尝试搜索CALL EAX,CALL EBX,JMP EAX等指令的机器码,因为很多壳是将OEP的值存放在寄存器中,然后通过CALL 某寄存器或者JMP 某寄存器来跳往OEP的。OllyBbg提供了搜索ALL COMMANDS的功能,我们可以通过单击鼠标右键选择-Search for-All Commands来搜索,然后各个指令处依次设置断点,下面我们来看个例子。 这里没有搜到任何记录,但是如果存在这样的指令的话,那么会在一个列表里面全部显示出来,我们就可以依次在每条记录上单击鼠标右键选择设置断点,当断下来的时候,我们可以看EAX的值为多少,根据EAX的值来确实是不是CALL或者JMP转移到第一个区段中。 这种搜索机器码的方式大家不是很常用,因为现在大部分的壳的解密例程都具有自修改功能,特别对于跳转到OEP这样的指令通常都是做了隐藏处理的,就是为了防止Cracker搜索机器码,但是,该方法对于一些简单的壳还是挺管用的。 2)使用OllyDbg自带的功能定位OEP(SFX法) 演示这种方法目标程序我们还是选择CRACKME UPX.EXE,用OD加载该程序,然后选择菜单项Options-Debugging options-SFX。 上图中SFX选项中用箭头标注出来的两个选项就是OllyDbg用来FZ定位OEP的,红色箭头标注的选项定位速度快,绿色箭头标注的选项定位速度慢,但是定位更加精确一些(PS:因为是按字节来定位的),我们来实验一下,选择红色箭头的选项。 重新加载该程序,会发现该选项并没有起作用,是因为如下原因: 我们可以看到OllyDbg帮助文档中的解释是,该选项只有当OllyDbg发现入口点位于代码段之外的时候才会起作用,而当前这个程序的入口点恰恰是位于代码段中的,所以OllyDbg的该选项就不起作用了。壳的入口点位于代码段中的情况还是比较少见的。 为了演示如何该选项在什么情况下能够起作用,这里给大家提供了一个名为UnPackMe_ASPack2.12的小程序,从名称上来看大家就应该可以猜到使用了ASPack壳。 首先我们使用Options-Debugging options-SFX中默认的选项,看看OllyDbg会不会检测到入口点位于代码段之外。 我们可以看到OllyBbg弹出了一个消息框显示入口点位于代码段之外。 我们单击Aceptar(确定)按钮。 到了壳的入口点处。 现在我们勾选上红色箭头标注的选项,并确保EXCEPTIONS菜单项中的忽略异常的选项都被勾选上了,重启OD。 运行起来。 我们可以看到停在了404000处,OllyDbg显示《Real entry point of SFX code》,即”真正的自解压代码的入口点”。(该程序是一个特例,OEP并不位于第一个区段,是位于第三个区段) 这里我们可以看到该选项起作用了,定位到了OEP,现在我们再来试试绿色箭头标注的选项,OllyDbg显示该选项定位OEP会很慢。 我们重启OllyDbg发现不一会儿就又停在了OEP处(PS:不是说很慢吗?),这是因为壳的解密例程太简短了,如果壳的解密例程很长的话,这两个选项的速度对比就能体现出来了。 这里大家要注意,在使用完该选项以后要记得恢复其默认的选项,如果不恢复默认选项的话,OD在分析其他正常的程序的时候就不会停在正常的入口点处了,进而影响程序的分析工作。 3)使用Patch过的OD来定位OEP(即内存映像法) 就是我们在VB章节中使用的那个Patch过的OD,即正常的内存访问断点读取,写入,执行的时候都会断下来,该Patch过的OD内存访问断点仅当执行的时候才会断下来,我们可以利用这一点来定位OEP,我们还是来看看CRACKME UPX,首先来看看区段列表。 UPX壳的解密例程会解密原程序的各个区段并将各个区段原始字节写回到原处,我们最好不要在解密区段的过程中断下来,说不定要断成千上万次才能到达OEP,这里有了这个Patch过的OD就方便多了,其内存访问断点仅当执行的时候才会断下来,当其在执行第一个区段中的代码时,基本上就可以断定是OEP了。 同样这里也要勾选上了忽略各种异常的选项,运行起来。 这种方法可能有点慢,因为设置的是内存访问断点(PS:写过调试器的童鞋应该知道,内存断点的机制决定了其慢的特性)。这里定位OEP的过程快则几秒钟,慢则几分钟不等(PS:大家可以去泡杯咖啡慢慢等,嘿嘿)。 不一会儿我们会发现定位到了OEP。 接着我们再来看看UnPackMe_ASPack2.12。 运行起来。 我们可以看到执行的第一行是这里,我们按下F7键看看会发生什么。 我们可以看到还是返回到了壳的解密例程中了,我们再次运行起来,程序直接运行起来了,为什么会这样呢?
因为这里壳的解密例程并不是将原程序代码段解密到第一个区段中,所以我们可以继续给后面的区段设置内存访问断点,逐一排查。 我们再来解释一下整个流程,直接运行该CrackMe的效果如下: 如果弹出了这个窗口就表示该程序在内存中解密区段完毕了,原程序的代码得以执行,我们依次给各个区段设置内存访问断点,然后运行起来,如果程序直接运行起来了,就证明这个区段不是我们要定位的,继续给下一个区段设置内存访问断点。 这里由于给第一个区段设置内存访问断点断了下来,但是继续执行程序就运行起来了,所以我们继续给第二个区段设置内存访问断点,当我们给第三个区段设置内存访问断点的时候其断在了404000地址处。 这里我们可以看到断在了OEP处,如果大家还不放心的话,继续运行,会发现继续断在下一条指令处,可以进一步断定这里是OEP了。 这种方法对很多壳都有效,嘿嘿。 4)堆栈平衡法(即ESP定律法) 这种方法适用于一些古老的壳。这些壳首先会使用PUSHAD指令保存寄存器环境,在解密各个区段完毕,跳往OEP之前,会使用POPAD指令恢复寄存器环境。 这里我们来看看CRACKME UPX。 我们可以看到第一条指令就是PUAHAD,有的情况下保存寄存器环境可能不是第一条指令,但也在附近了,还有些情况下,有些壳不使用PUSHAD,而是逐一PUSH各个寄存器(例如:PUSH EAX,PUSH EBX等等),总而言之,在解密完区段,跳往OEP之前会恢复寄存器环境。 这里我们按F7键执行PUSHAD: 可以看到各个寄存器的初始值被压入到堆栈中了,这里我们可以对这些初始值设置内存或者硬件访问断点,当解密例程读取这些初始值的时候就会断下来,断下来处基本上就在OEP附近了。 这里我们可以通过在ESP寄存器值上面单击鼠标右键选择-Followin dump在数据窗口中定位到这些寄存器的初始值。 数据窗口中显示的堆栈内容如下:
这里我们可以对这些初始值的第一个字节或者前4个字节设置硬件访问断点。 选择字节,字,双字都可以,只要解密例程在读取这些值的时候断下来就OK,运行起来。 我们可以断在了POPAD指令的下一行,当壳的解密例程读取该值的时候断了下来,紧接着下面就是跳往OEP处,说明这个方法起作用了。 我们再来看看UnPackMe_ASPack2.12。 我们可以看到第一行也是PUSHAD,我们依然按F7键执行PUSHAD,然后还是通过在ESP寄存器值上面单击鼠标右键选择-Followin dump在数据窗口中定位到这些寄存器的初始值。 运行起来。 我们可以看到断在了跳往OEP PUSH 404000指令之前,我们继续按F7键单步。 可以看到到了OEP处。 这里要给大家说明一点,现在很多壳都能检测这种方法,所以说大家可以多多汲取一些方法和经验,尝试不同的方法,才能知道那种方法最合适。 我们继续来看其他定位OEP的方法。 5)VB应用程序定位OEP法(Native 或者P-CODE) 定位VB程序的OEP比较容易,因为VB应用程序都有一个特点-开始都是一个PUSH指令,紧接着一个CALL指令调用一个VB API函数。我们可以使用Patch过的OD,首先定位到VB的动态库,接着给该动态库的代码段设置内存访问断点, 当壳的解密例程解密完原程序各个区段,接着就会断在VB DLL的第一条指令处,接着我们可以在堆栈中定位到返回地址,就可以来到OEP的下一条指令处。这里我们也可以使用前面介绍的方法-跟逐一给各个区段设置内存访问断点(使用Patch过的OD),但是很多壳会检测这种方法,所以大家可能根据需要不同的情况来尝试这不同的方法。这种方法很容易理解,我就不举例子了,以后大家如果遇到了VB程序可以试试这种方法。 6)最后一次异常法 如果我们在脱壳的过程中发现目标程序产生大量异常的话,就可以使用最后一次异常法,我们来看一个例子,名字叫做”bitarts_evaluations.c”。 我们还是使用Patch过的OD来加载它,并且配置好反反调试插件。 然后将EXCEPTIONS菜单项中的忽略各个异常的选项都勾选上,运行起来。 我们可以看到程序运行起来了,我们单击工具栏中L按钮打开日志窗口。 这里我们可以看到产生了好几处异常,但是都不是位于第一个区段,说明这些异常不是在原程序运行期间发生的,是在壳的解密例程执行期间产生的异常,最后一次是46e88f处的这个异常。 好,现在我们重新启动OD,将EXCEPTIONS菜单项中忽略的异常选项的对勾都去掉,仅保留Ignore memory access violations in KERNEL32这个选项的对勾。 我们运行起来,产生异常断了下来,我们直接按SHIFT + F9忽略异常继续运行。直到停在了46E88F处为止。 这里不是,我们按SHIFT +F9忽略异常继续运行,我们知道最后一次异常是46E88F处的INT 3指令引发的。 这里是壳的解密例程执行过程中产生的最后一次异常,接着就是执行原程序的代码了。 接着我们可以对代码段设置内存访问断点,可能有人会问,为什么不在一开始设置内存访问断点呢?原因是很多壳会检测程序在开始时是否自身被设置内存访问断点,如果执行到了最后一次异常处的话,很可能已经绕过了壳的检测时机,我们来试一试。 我们按SHIFT + F9忽略该异常运行起来。 我们可以看到断在了OEP处,下面我们来看看该壳在开始的时候是否有检测内存访问断点。 我们重新加载该程序,将忽略的异常选项都勾选上,接着打开区段列表窗口,给第一个区段设置内存访问断点,过了很久断在了OEP处。 虽然这里我们直接给第一个区段设置内存访问断点直接定位到了OEP,但是了解某些壳会检测内存访问断点还是非常有必要的,如果我们在离OEP越近的地方设置内存访问断点,就越不容易被壳检测到。 好了,现在我们再来试试第二种方法中介绍的OD自带的功能选项是否能够定位到OEP。 同样定位到了OEP。 现在我们来试试第四种方法中介绍的ESP定律,我们观察一下该壳的入口点: 我们按F7键单步跟踪几行就能到达PUSHAD指令处。 我们按F7键执行PUSHAD指令,接着在ESP寄存器值上面单击鼠标右键选择-Followin dump在数据窗口中定位到寄存器的初始值。 给前4个字节设置硬件访问断点,运行起来。 这里就断在了恢复寄存器环境的指令的下一行,我们按F7键单步执行到RETN处,接着再单步一下就能到达OEP处。 7)利用壳最常用的API函数来定位OEP 我们还是用Patch过的OD加载bitarts_evaluations.c。将忽略的异常选项都勾选上,我们来定位一下壳最常用的API函数,比如GetProcAddress,LoadLibrary。ExitThread有些壳会用。我们首先来看看GetProcAddress。 我们可以看到该壳使用了GetProcAddress,接着使用bp GetProcAddress命令给该API函数设置一个断点。 如果在命令栏中使用bp命令设置断点失败的话,可以尝试手工设置断点。 运行起来。 这里我们并不需要其断下来,我们只需要知道壳在哪些地方调用GetProcAddress,所以我们在断下来的这一行上面单击鼠标右键选择-Breakpoint-Conditional log,来设置条件记录。 这里我们将Pause program这一项勾选上Never,记录的表达式设置为[ESP],也就是记录返回地址,这样我们就能知道哪些地方调用GetProcAddress。接着在日志窗口中单击鼠标右键选择-Clear Log(清空日志)。 运行起来,我们可以看到程序的主窗口弹了出来,打开日志窗口,看看最后一次GetProcAddress(排除掉第一个区段中调用的位置)是在哪里被调用的。 我们可以看到基本上GetProcAddress都是解密例程中调用的,除了428C2B这一处以外(这里是第一个区段中调用的,也就是原程序本身调用的)。所以我们要定位的应该是47009A这一处。接下来我们重新来编辑一下条件断点中断的条件,将中断条件设置为[ESP] == 47009A。 并且将Pause program这一项勾选上Oncondition。 重新启动OllyDbg。 编辑条件断点。 设置Condition为[ESP] ==47009A,接着将Pause program这一项勾选上Oncondition。 运行起来。 断了下来。我们可以在对代码段设置内存访问断点之前尝试一下这种方法,这样就可以绕过很多壳对内存断点的检测,但是有一些壳也会对API函数断点进行检测,所以说我们需要各种方式都尝试一下,找到最合适的。 对当前这个壳定位GetProcAddress的调用处是可行的,我们现在已经在OEP附近了。如果定位GetProcAddress的调用处失败的话,我们可以换其他的API函数,这里我们再来看看日志窗口,可以看到一处线程结束记录。 因此接下来给ExitThread设置断点,并且将菜单项Debugging options-Events中的Break onthread end(在线程结束位置中断下来)勾选上。 运行起来。 断在了线程结束的位置。 接着我们给代码段设置内存访问断点就能够马上定位到OEP。 8)利用应用程序调用的第一个API函数来定位OEP 这种方法就是直接给应用程序调用的第一个API函数设置断点,比如说,很多程序(VC++)一开始会调用GetVersion,GetModuleHandleA,对于bitarts_evaluations.c来说我们可以断GetVersion,对于CRACKME UPX来说我们可以断GetModuleHandleA。这里是bitarts_evaluations.c,所以我们给GetVersion设置断点。 运行起来。 这里我们断在了GetVersion的入口点处,从堆栈中我们可以看到返回地址位于第一个区段。我们直接在返回地址上面单击鼠标右键选择-Follow in Disassembler。 这里我们又定位到了OEP。以上就给大家演示的如何利用应用程序调用的第一个API函数来定位OEP了。如果我们遇到有的壳检测GetVersion入口处的INT 3断点的话,我们可以尝试在该API函数的返回指令RET处下断。 其实还有很多适用于特定壳定位OEP的方法,这里就不给大家一一介绍了,基本上也是根据上面的这些基本方法变通来的,所以说大家掌握好上述这些基本的定位OEP的方法和原理就即可。 这里给大家留一个小程序练习,名字叫做UnPackMe_tElock0.98。大家尝试定位其OEP。这个壳专门采取了一些技巧来干扰利用上述方法定位OEP,所以说如果直接利用上述这些方法的话,就不能奏效了,大家可以好好琢磨一下这个壳。 大家记住,如果壳检测INT 3断点或者硬件断点的话,你使用ESP定律给堆栈中的寄存器初始值设置硬件断点也是不起作用的,只能换其他方法。 接下来的章节将介绍转储(dump),以及如何修复IAT等知识点。
本系列文章汉化版转载看雪论坛
感谢原作者:RicardoNarvaja(西班牙人)
感谢热心翻译的朋友: 1~3章译者:BGCoder 4~58章译者:安于此生
全集配套程序下载地址:
|