使用OllyDbg从零开始Cracking 第四十九章-PeSpin V1.3.04脱壳-Part2
第四十九章-PeSpin V1.3.04脱壳-Part2本章我们来修改PeSpin的IAT。当我们到达伪造的OEP处时,我们随便定位一个API函数调用处,接着定位到IAT,通过观察很容易得知IAT的起始地址为460818,结束位置为460F28。但是我们会发现一个问题:有些CALL并没有调用IAT中的项。我们一起来看个例子。这里整个IAT我都用浅绿色高亮标注出来了,但是我们会发现IAT中的有些项找不到参考引用的地方(比如说这些0006开头的值),这些值并不指向任何API函数,而且也不在任意一个DLL的地址空间范围之内,那么也就是说这些值不是重定向过的就是垃圾数据了。现在我们来查看所有的API函数调用处,在反汇编窗口中单击鼠标右键选择-Search for-All intermodular calls。我们会发现很多CALL读取的都不是IAT中的值,倒像是另一张表中的值(PS:这里为了方便下文的描述,我们姑且称这张表为IAT2)。下面我们就在数据窗口中来定位到IAT2,我们随便选中一个CALL,单击鼠标右键选择Follow in Disassembler,在反汇编窗口中定位到该CALL。我们来到了这里。下面我们在数据窗口中定位到该IAT2中的项。这里我们可以看出一些规律,我用浅绿色高亮标注出了连续排列的3项,项与项之间是用零隔开的,这里大概可以知道,IAT2中的元素是重定向到各个API函数的。我们单击鼠标右键选择Follow,看看其会最终会重定向到哪个API函数。经过几次跳转之后来到了这里。又经过几次跳转以后我们来到了这里:大家细心观察的话,会发现这里我用浅绿色标注出来的这三条指令在前面壳创建的区段中已经执行过了(不记得的话可以按减号键往回翻看),这样做的目的很可能是防止逆向者通过在API的入口点处设置断点以此来判断是不是被重定向到了该函数。这里如果我们想知道该API函数是什么的话,直接在选中MOV EDX,DWORD PTR SS:这条指令,单击鼠标右键选择New origin here,将EIP指针指向该指令。(PS:其实不需要这样做,我们可以直接将反汇编窗口中内存地址与机器码中间的那一条线往右边拖动,就可以看到API函数的名称为RtlLeaveCriticalSection了)这里我们可以看到EIP寄存器的右边显示出了该API函数的名称为RtlLeaveCriticalSection。好了,这里我们还是将EIP的值恢复吧,以免出错。好了,这里我们就知道IAT2中的项的确是重定向的IAT项。也就是说该壳不会去调用IAT中的那些0006开头的值(这些值是该壳可以构造的,也可以说是混淆视听吧),那么0006开头的值占据的IAT项怎么办呢?对于这些项该壳会调用IAT2中相应的项,这么做的目的无疑是为了增加我们修复IAT的难度。我们该如何来修复IAT呢?首先还是来看到42891E地址处的这个CALL(PS:这里该地址大家可能各不相同,以自己机器上的为准)。我们已经知道了该CALL实际上调用的API函数是RtlLeaveCriticalSection。我们可以利用RtlLeaveCriticalSection这个函数入口地址来做做文章,首先第一步,我们需要定位到46F525这一项的值00A205EC是哪里写入的。我们定位到了46F525这一项写入指令处以后,直接将写入指令中的00A205EC替换成7C9110ED(RtlLeaveCriticalSection的地址)即可。这里我们需要想个办法让其直接调用正确的API函数,也就说我们需要将该IAT项中的内容由00A205EC替换成7C9110ED。首先我们要知道该项是由哪里写入的话,我们需要给该项设置一个硬件写入断点。下面我们就来看看00A205EC这个值是哪里写入到46F525中的,现在我们重启OD。断在了入口点处,我们运行起来。中间会由于写入46F525断几次,但是因为写入的值并不是00A205EC,所以我们继续运行,运行几次以后,我们发现这里写入的是00A205EC,由于硬件断点是断在下一条指令处,所以我们来看看前一条指令是什么。这里的代码被混淆了,看不出个所以然来。如果想清楚的看到前一条指令到底是什么的话,我们可以选中前面的一个JMP指令,然后单击鼠标右键选择Follow。这样就可以清楚的看到前一条指令是什么了,重定向到IAT2中值来至于EAX寄存器,我们来看看EAX此时的值。我们可以看到的确是00A205EC(PS:每个人机器上可能该值不同,以自己机器的为准),好了,我们已经定位到了关键点了。接下来我们给MOV DWORD PTR DS:,EAX这一条指令设置一个硬件执行断点。我们运行起来,再次断了下来,会发现继续在写入46F52E后面的IAT2中的项。我们观察一下寄存器的情况,会发现EAX中正好保存的是另一个重定向的值。我们继续运行,会继续写入下一个重定向的值。这里我们注意观察EDI值的变化,我们会发现EDI的值是递增的,指向的都是IAT2表中的每一个元素,再看看EDX的值,EDX指向的正好是IAT中的项,好,现在我们重启OD,一直运行,直到断到写入46F52E的前一项为止。好了,这里由于写入46F529所在内存单元的时候断了下来,即46F52E的前一项,这里我们知道下一项要写入的是RtlLeaveCriticalSection这个API函数重定向过的值,这里我们不继续按F9运行了,我们利用OD自带的跟踪功能来协助我们分析,下一项的正确的IAT项值应该7C9110ED(RtlLeaveCriticalSection这个API函数的首地址)(PS:以自己机器上的地址为准),这里我们来看看哪个寄存器中会出现7C9110ED这个值,我们利用OD的自动跟踪功能来定位这个值。我们来设置一个自动跟踪的条件:EAX == 7C9110ED || EBX == 7C9110ED || ECX == 7C9110ED || EDX == 7C9110ED || ESI == 7C9110ED || EDI == 7C9110ED||表示任意一个条件成立即为真。也就是说当以上寄存器组中任意一个的值等于7C9110ED 的时候就会停下来。这里我们被忘了勾选上Debugging options-Trace菜单项中的Always trace over system DLLs,Always trace over string commands(这个选项的意思就是遇到诸如REPS这样的字符串循环操作指令的时候直接跳过)这两项,因为如果让OD自动跟踪系统DLL或者字符串操作指令的话耗费的时间可能会非常长,接下来我们选中Debug-Trace into菜单项开始自动跟踪。稍等片刻就会停在这里。我们可以看到这是一个CALL的返回地址,我们来看看跟踪的日志信息。我们双击日志中的这一项。(PS:这里我的日志信息中并没有记录CALL NEAR DWORD PTR SS:这一项,这里由于我的日志跟踪信息中没有记录这条指令的缘故,所以我这里也显示不出GetProcAddress这个API函数名称,但是影响并不大)这里明显可以看出壳在获取API函数的地址,接着会重定向到自己创建的区段中。接下来我们来定位哪里会写入00A205EC这个值。我们将跟踪的条件修改为:EAX == 00A205EC || EBX == 00A205EC || ECX == 00A205EC || EDX == 00A205EC || ESI == 00A205EC || EDI == 00A205EC我们再次选择Debug-trace into菜单项进行自动跟踪。停在了这里,此时EAX指向的是正确的API函数地址,而ESI指向了重定向过的值,嘿嘿。这个点非常完美。我们删除之前创建的硬件断点。接着给MOV DWORD PTR SS:,ESI这一条指令处设置硬件执行断点。我们重启OD,然后直接运行起来,可以看到第一次就断在了这里。我们可以看到此时EAX同样指向了一个正确的API函数-GetSystemTime,而ESI寄存器也同样指向了一个重定向过的值,所以我们来Patch这条指令以此来修复IAT2。这里我们将MOV DWORD PTR SS:,ESI这条指令修改为MOV DWORD PTR SS:,EAX,这样就用正确的API函数地址替代了重定向过的值,接着我们删除之前设置的硬件断点,运行起来。我们来看看IAT2发生了什么变化。好,我们可以看到现在IAT2中保存的都是正确的API函数地址了。我们看下46F52E调用指令的情况试试:这里我们可以看到调用的确是正确的API函数了,不再是重定向的值了。但是这里仍然是是IAT2中的值被修复了,IAT中哪些0006开头的无效的项仍然没有被修复,所以下一步我们要将IAT中无效的值修复。现在我们有两个切入点,一个就是是46BBC4这一条指令,我们需要将ESI修改为EAX,这样就可以确保正确的API函数地址被填充到IAT2中。另一个切入点是46C010这一条指令,我们分别给该这处指令设置硬件执行断点,接着重启OD。断在了这里,我们将ESI修改为EAX。我们运行起来。断在了这里,这里是将正确的API函数地址填充到IAT2中,此时EDX指向了对应的IAT项,而EDI指向了IAT2中对应的项,现在是我们修复IAT的绝佳时机,我们做如下操作即可:我们知道上面这条JMP指令是直接跳转到MOV DWORD PTR DS:,EAX这条指令的,这里压根我们就不需要其无条件跳转,这条指令相当于是费的,我们将其NOP掉。如下:这里我们在上面添加一条指令MOV DWORD PTR DS:,EAX,这样可以将正确API函数地址就被写入到了IAT中了。这样IAT中和IAT2两张表中都将保存的是正确的API函数地址,现在我们删除掉之前设置的硬件断点。给主程序代码段设置内存访问断点,运行起来,不一会儿就能断到伪造的OEP处。这里我们就断到了伪造的OEP处,我们随便在下面找一个CALL, 分别看看IAT和IAT2的情况如何。这里我们可以看到IAT2中保存到都是正确API函数地址了。那么IAT中呢?这里我们可以看到IAT中之前那些0006开头的值也已经被修复了。好了,下面我们要做的就是将代码段中的这些间接CALL IAT2中的项转化为CALL IAT中的项,下面我们就通过一个简单的脚本来完成。我会给大家逐一介绍每一条语句是干什么用的,但是在解释脚本之前我们先来修复一下stolen bytes。这里脚本我已经写好了,可能不是最优的,但是已经足够用了,名称叫做Spin.txt。首先:var var_callvar var_tablevar var_apivar var_iatvar var_programvar var_end这里是声明该脚本中需要用到的一些变量。下面我会给大家介绍每个变量的功能。mov var_program,401000mov var_iat,460818这里是初始化变量,将主模块代码段的起始地址赋值给变量var_program,因为我们要从主模块的代码段起始地址处开始定位CALL。接着将IAT的起始地址赋值给变量var_iat。TheStart:findop var_program,#FF15??#log $RESULT这里才是我们脚本真正开始执行的地方。TheStart是一个标签,顾名思义:开始。接下来是通过findop 命令从401000地址处开始查找以FF15开头的间接CALL,??表示通配符。如果查找成功,地址将会保存到保留变量$RESULT中,否则$RESULT将等于0。接下来通过log 命令将$RESULT的记录到OD的日志窗口中,这个命令不是必须的,只是为了方便我们的查看。mov var_call,$RESULTcmp var_call,0je TheFinalcmp var_call,44904Bjae TheFinal这里首先将查找到结果$RESULT赋值给变量var_call,接着判断查找的结果是否为0,如果为0,表示没有找到以FF15开头的间接CALL,那么就直接跳转到TheFinal标签处结束该脚本的执行,如果找到了,判断间接CALL指令的地址是否高于代码段中最后一项间接CALL指令所在的地址,如果高于,同样跳转到TheFinal标签处结束脚本的执行。TheFollow:add var_call,2log var_callmov var_table,log var_tablemov var_api,log var_apicmp var_api,50000000jb TheJmp这里将间接CALL指令的地址+2,然后读取4字节的内容保存到变量var_table中,也就是IAT2中的表项,接着读取表项的值也就是读取API函数的地址,判断其是否大于50000000,如果大于说明是正常的API函数地址,如果小于就说明不是正确的API函数地址,我们跳转至TheJmp标签处继续定位下一个间接CALL。TheLoop:cmp var_api,je TheSolveadd var_iat,4cmp var_iat,460F28jae TheJmpjmp TheLoop我们现在有IAT2中正确的API函数的地址了,所以接着我们就需要查询IAT中跟其一致的API函数地址,如果是API函数地址相等的话,就可以将该间接CALL中的IAT2中的项替换成IAT中的项了,但是如果查遍整个IAT都没有查找到与之相等API函数地址的话,就跳转到TheJmp标签处进行定位下一个间接CALL,如果在IAT中查找到了相等的API函数地址,就跳转到TheSolve标签处进行替换工作。TheSolve:log var_iatlog var_callmov ,var_iat这里将IAT2的项值替换成对应的IAT的项值。stimov eip,4271F7这两行其实其实不起作用,但是由于OllyScript插件要求程序必须执行一些东西,不能单单的进行搜索或者更改值,或者挂起什么也不做,所以我们不得不添加一个STL命令,让其单步执行,相当于F7,然后再将eip指向伪造的OEP处。这里OD有一点不好的地方,就是当有些脚本Patch过的字节超过1000的时候就弹出一个消息框进行提示,我们点击几次OK以后脚本就执行完毕了,IAT也就修复成功了。好了,现在我们来dump,然后用IMP REC修复IAT。这里我们发现存在一个无效的项,我们直接将其剪切掉。接着我们修复刚刚dump出来的文件,我们会发现无法运行,好像还缺少点什么。这里存在AntiDump,我们会发现入口点下面存在一些CALL,这些CALL的地址属于PE头,而不是代码段。我们单击鼠标右键选择Search for-All intermodular calls。查看所有的API函数的调用处。我们定位到无效的CALL。我们可以看到这些是需要我们修复的。但愿不需要我们编写第二个脚本来进行修复。现在我们需要在刚刚dump并修复了IAT的文件中定位1000(十六进制)个字节的空间用于存储PE头中有用的信息。我们用OD加载刚刚dump并修复了IAT的那个文件,随便定位一个1000(十六进制)字节的区域,这里我找的区域起始地址为45CAB0。这里我们需要验证一下这块虚拟地址空间是否在文件中存在,我们单击鼠标右键选择View-Executable file:我们可以看到这块虚拟地址空间在文件中的确存在,现在我们需要从之前那个断在OEP处的OD中拷贝PE头中的1000(16进制)个字节出来。选中整个区段。二进制复制并粘贴到dump文件所在OD的45CAB0为起始地址,长度为1000(16进制)字节的区域中。并且将修改保存到文件。现在PE头的数据我们是拷贝过去了,下面我需要在程序开始运行起来,起始地址为45CAB0,长度为1000这部分数据覆盖到PE头中以此来修复AntiDump。但是现在有个问题呀,PE头具有可写权限吗?PE头并一定具有可写的权限,那么我就需要给其赋予可写权限,我们通常会调用VirtualProtect这个API函数来修改内存的访问权限。但是该程序的IAT中并没有VirtualProtect这个API函数对应的项。怎么办呢?通常为了兼容性考虑会使用LoadLibrary和GetProcAddress这两个API函数来配合获取VirtualProtect的地址。但是这里我会告诉你一个更加快捷的方式。假设LoadLibraryA是IAT中的一项,这里LoadLibraryA的实现代码我已经用绿色高亮显示了。其实几乎所有的XP系统上LoadLibraryA与VirtualProtect的入口地址的差是一个定值。我们只需要通过LoadLibraryA的首地址减去这个定值就可以定位到VirtualProtect的首地址。这里我们定位到IAT中的LoadLibrary这一项。这里首先将LoadLibraryA的地址存放到EAX中。下面我们来计算一下LoadLibraryA与VirtualProtect这两个函数入口地址之间的差值。这里我们可以看到LoadLibraryA与VirtualProtect这两个函数入口地址之间的差值为2A7。获取到VirtualProtect的入口地址以后,我们就可以对PE头赋予写入权限了。这里是MSDN中对于VirtualProtect这个函数的说明。我们首先需要一个PUSHAD指令保存寄存器环境,接着将LoadLibrary函数的入口地址保存到EAX中,接着将其减去2A7就得到了VirtualProtect这个函数的入口地址。接着调用VirtualProtect给PE头赋予写入权限。该函数的参数如下:首先是要修改的访问权限的起始地址为400000,长度为1000,新的访问权限为可读可写可执行,旧的访问权限保存到45CA50所指向的内存单元中。接着将起始地址为45CAB0,长度为1000的数据拷贝到起始地址为400000,长度为1000的内存单元(也就是PE头)中。接着调用POPAD指令还原寄存器环境,然后跳往OEP处。现在要做的事情就是将入口点修改为45CA10了。这里我们将AddressOfEntryPoint修改为5CA10即可(RVA)。保存修改到文件,直接运行起来。嘿嘿,搞定。本系列文章汉化版转载看雪论坛
感谢原作者:RicardoNarvaja(西班牙人) 原作者个人主页:http://www.ricardonarvaja.info/
感谢热心翻译的朋友:1~3章译者:BGCoder4~58章译者:安于此生
全集配套程序下载地址:
链接: http://pan.baidu.com/s/1eQzTWfo 密码: vytv
[快捷回复]-感谢楼主热心分享! 真是太厉害 谢谢恒大分享 太牛了,必须赶紧下载!!!! 不错哦,可以学习一下 看帖子内容.....好评+1 感谢楼主,马上尝试一下!
页:
[1]