ARTeam 教程
作者:
Shub-Nigurrath 请访问:
http://cracking.accessroot.com
|
http://forum.accessroot.com 新手教程第 #4 篇: 脱壳与补丁, 一个稍复杂点的例子 v.1.1 |
相关信息 | Gabri3l发起的新手系列教程的继续,解释了aspack的压缩程序,IAT细节和Olly的自解压功能. |
目标 | Super Video Joiner 1.8.0 |
下载地址 |
http://www.witcobber.com/ 或者 http:/www.intechhosting.com/~access/ARTeam/tools/superjoiner_180.exe |
工具 | OllyDbg 1.10 (HideDebug和OllyDump插件), Import Reconstructor 1.6, Diablo2oo2 Universal Patcher |
保护方式 | AsPack 2.12,被破坏的输入表, 序列号保护 |
难度 | 新手 |
类别 | 补丁 |
作者(们) | Shub-Nigurrath 2005年二月 |
软件要求 | Windows XP, Firefox 1.0 或更高(以获得更好的阅读效果和打印效果) |
1. 介绍
|
这篇教程中我将(滥)用另外一个由AsPack保护的程序,但这个程序却并不能用Gabri3l在他的第三篇文章中提到的方法脱壳.这次的目标软件是Super Video Joiner,一个用来将不同几个的视频合并成一个文件的小程序,也可以是DivXs. 我们不久就会看到,这个目标程序有一个不同的输入地址表(ImportAddressTable,IAT)格式,这需要我们使用Import Reconstructor的高级一点的用法.我还会演示如何使用Olly自动脱那些带自解压程序的壳(比如那些用aspack, upx或者类似的程序但不是asprotect, armadillo等程序压缩的程序).而一旦脱了壳,对脱壳后的程序进行补丁就是非常简单的了,我将只用几步将其讲述清楚. 最后我将集中在如何创建一个小补丁上,希望这补丁会在将来的版本中继续有效:Gabri3l展示了如何使用Dup(Diablo2oo2Universal Patcher)的偏移量补丁(Offset Patch),而我要讲述的的是如何使用搜索与替换(Search and Replace)这一页的功能... 好了,系好安全带(出发!) ;-) |
2. 程序的脱壳
|
顺便提一下,PEiD不会总是100%正确,偶尔也会犯错误.因此它的作者加入了允许使用外部签名数据库的功能,这就是在它的安装目录下的叫做userrdb.txt外部文件. 该文件包含了PEiD内部当前不包含的额外签名.这些签名通常都会在各处的论坛中发布出来,甚至我们的论坛上也有,你可以花一些时间来收集这些签名,这样会提高你的PEiD的准确度.PEiD只安装一个没有任何意义的示例userdfb.txt文件,所以通常你需要四处找一些真正的签名.
话说回来,这次是个简单的情况,AsPack很容易就被PEiD识别了出来.你大概早就知道了AsPack是一个程序压缩壳,而不是保护壳,所以它的主要目的是减小整个exe文件的大小而不是保护它.它的主要特征是可以"re-entrant",也就是说解压代码将程序解压到内存中后它不会留下任何跟踪代码,在程序看来就好像什么也没有发生一样(同样见Gabri3l的教程).这样就有了Gabri3l在他的教程中所推荐的方法,也就是在第一个PUSHAD指令后的堆栈中的Dword设置一个硬件断点. 另一方面,像AsProtect这样的保护壳(与AsPack同一作者),不仅仅只对可执行代码作那么几件事而且是"非re-entrant"的;另外当程序在内存解压后要执行时,其代码已经被大面积的被修改(它的IAT被修改是为了使其不容易被重建,它的指令被修改是为了使其不容易被跟踪出程序的流程等等).
让Ollydbg为我们解压程序
在这篇文章里我要用另一种方法,这种方法已经OllyDbg在中实现了,可是却很少有人知道或用到. 进入OllyDbg的选项然后设成下图这样:
OllyDbg有个自解压功能模块(Self Extractors,SFX),它所完成的和Gabri3l向你讲述的方法相似(实际上更复杂些).正如我所说的,这个技巧对所有的"re-entrant"(原来的程序可以重获控制权)的程序压缩壳都有效,或者说的更广泛些,它对所有的在内存中自解压的可执行程序都有效.Ollydbg能够为你做所有的工作并且停在正确的地方! 像上面那样设置好了之后就可以将目标载入Ollydbg了(这一次的目标程序是videojoiner.exe). 没必要关掉Olly再打开,只要重新载入就可以了,也就是说如果已经打开了的话再按一下CTRL-F2就可以了. 通常Ollydbg都会停在AsPack解压代码的第一条指令上(如下),是一条PUSHAD指令
004F8001 > 60 PUSHAD
你可以看到,Olly会在状态栏中告诉你它所作的工作的一个统计数据:
如果你还没看到,按一下CTRL-A重新分析程序,这样就会清楚地看到我们已经到了程序的真正入口处(OEP). 关于Ollydbg需要注意一点:Ollydbg会存储(存在UDD文件中)刚才找到的真正入口(OEP),这样下次我们启动该程序的时候就会立刻找到OEP, Ollydbg会报告SFX入口点(也就是OEP)已经"得到了":
可以看到,我们所停的地方与我们手动获得的地方是一样的!所以,就省去了一些工作!
脱壳进程以及修复输入地址表(IAT)
实现用OllyDump将程序脱壳,不要选中重建输入表复选框Rebuild Import,注意一下OEP. 这里就是: AF0A0.
我把脱了壳的文件起名, dump.exe. 现在打开Import Reconstructor 1.6,从进程列表中打开你正在调试的程序. 我会做得简单些,如果你已经对这个程序很熟悉了,那么下面这个图就足以说明一切了,图中的数字是所要进行的操作步骤.
估计你已经知道了,这就是ImpRec认为IAT所在的地方(IAT有一个固定的结构所以很容易被ImpRec识别出来). 进行到图中的第3步和第4步你就会得出结论:这次IAT是不可能用这种方法重建的! 我们需要指导ImpRec以另一种方式在进程的内存空间中自己找到API函数!
来看看怎么做.. 右击界面上的空白处会有这样一个菜单:
理解ImpRec将要做的工作 下面就应该是你看到的:
004AF0A0 /. 55
PUSH EBP ; Real entry point of SFX code 看一下这个调用: 是这个样子的CALL videojoi.00406C94,此处00406C94是调用的子程序的地址. 在这个地址我们看到:
00406C94 $-
FF25 04474B00 JMP DWORD PTR DS:[4B4704] ;
user32.FindWindowA 让我们做个总结: 有一个对某地址(004AF0D6)的调用,这个调用包含了跳向另一个地址(004B4704)的JMP,所以我们可以将第一个调用地址看作一个指向真正调用地址的指针,在汇编中这就是一个间接调用. 它的参数是这样的:
或者干脆用ImpRec的表示方法就是, CALL [X]..ImpRec现在要你做什么?选中"Get CALL [X] Scheme"... 比如说当寄存器中包含要调用的子程序的地址时,对应的汇编指令就会是这个模样:CALL [EAX], 假设EAX是这个寄存器. 让我们歇歇来更深入的看一下这个特别的参数, 首先,了解一下编译器是如何生成输入函数的这种调用的,以及为什么要这样做,我觉得会很有意义. 仔细想想输入API函数的调用是什么样子的,我们会想到两种情况:一种效率高的办法和一种效率不高的办法. 好一点的情况,输入API函数的调用应该是这样的: CALL DWORD PTR [0x00405030] 如果你不熟悉x86汇编语言,我告诉你这是一个函数指针的调用.不管0x405030处DWORD大小的值是什么,CALL指令都会将程序的控制权转移到那里. 在前面的例子里, 0x405030这个地址就在输入地址表IAT中.
CALL 0x0040100C ........ 0x0040100C: JMP DWORD PTR [0x00405030]这种情况下, CALL指令将控制权交给一个stub. 由这个stub跳转到0x405030处所存储的地址. 同样,需要注意0x405030是输入地址表IAT中的一项 . 简而言之, 效率低的方式调用API函数多使用5个字节,并且因为这个多余的跳转执行是也会占用更多的时间.
那你可能会疑问怎么这种方式既然效率低却还要用呢? 没有很好的解释. 由于编译器本身的问题,它并不能区分同一模块中的API输入函数和普通函数的调用. 这样, 编译器就生成了这种形式的调用
CALL XXXXXXXX此处 XXXXXXXX 是由连接器填充的实际的代码地址. 注意到上面这个调用指令没有借用函数指针, 而是使用了实际的代码地址. 为了保持全局内前后的环境平衡, 连接器需要一大堆的代码来替换 XXXXXXXX. 最简单的实现方法就是使用一个对JMP stub的调用,这就是前面你所见到的. JMP stub是从哪里来的? 很让人吃惊, 它来自输入函数所在地输入函数库. 如果你研究一下输入函数库,研究一下与输入的API函数名相关联的代码,你会发现它正是一个上面那样的JMP stub. 这就意味着,在默认的情况下,不进行任何外来的干预,API输入函数的调用将会使用效率低的形式. 按这个逻辑,下个问题就是:如何得到优化的方式呢? 答案就在于你提供给编译器的一个暗示. __declspec(dllimport) 告诉编译器该函数在另外一个DLL中,这样编译器就会生成下面的指令
CALL DWORD PTR [XXXXXXXX]而不是这样的指令: CALL XXXXXXXX另外, 编译器会生成一些信息,告诉连接器指令的函数指针部分解析成一个叫做 __imp_functionname(__imp_函数名)的符号. 比如, 如果你要调用MyFunction, 符号名称就应该是__imp_MyFunction. 看一下输入函数库的内部, 就会发现普通的符号名前都附加有__imp__前缀.这个__imp__符号直接解析为输入函数表IAT项目,而不解析为JMP stub. 既然这样,在平时你应该怎么做? 如果你在写输出函数并且为它们提供了一个.H文件,别忘了对函数使用__declspec(dllimport)修饰符号:
__declspec(dllimport) void Foo(void);到Windows系统头文件里看看, 就会发现他们对Windows API同样使用__declspec(dllimport). 看到这一点并不容易, 但是如果你在WINNT.H中搜一下WinBase.H文件中使用的DECLSPEC_IMPORT宏的定义, 你就会发现 __declspec(dllimport) 是如何附加到系统API函数声明前面的.
将这些函数去除掉只需在其中一项上右击然后选择"剪切指针数据"("Cut Chunk(s)" )(见前面图中的右键菜单). 再次按下"显示无效函数"("Show Invalid"),这次应该没有什么无效项了. ImpRec的日志窗口显示 "Congratulation!" 不再有无效指针了, 可问题是:这能行么?:-)
按下"修复抓取文件(Fix Dump)" 马上我们就会看到答案了 ;-) 很明显答案是YES, 程序正常工作了! |
3. 给程序打补丁
|
现在我们已经得到了一个正常工作的脱壳文件,可以痛痛快快地给它打补丁了. 先研究一下程序的保护方式:程序有三个限制,不能合并两个以上的文件,退出时有个消息框,不能输入任意的注册码. 所有这些限制都有一个明显的错误消息框, 而这正是用Olly逆向时很重要的条件. 从这一系列前面的新手教程以及ARTeam教程里, 你应该早知道了错误消息框是发现需要打补丁的代码的好地方, 也应该学会了如何在OllyDbg中搜索参考字符串. 用这种方法我发现了程序中需要补丁的三个地方. 我只是将代码简单的列了出来,并没有做什么注释,因为它们实在是太简单了 (只是修改一些跳转指令). 但我还是列出了我找到这些地方所用的参考字符串. 补丁1 修改后的代码: 补丁2 修改后的代码: 补丁3 修改后的代码: 完成了么! 希望你能理解我只是简单粘到这里的代码, (这样做我也很抱歉,但是尽量缩短教程的长度也很重要).在Ollydbg保存程序所作的修改并起个名字比如dump.patched.exe, 然后运行. 程序正常工作了,没有任何限制,没有恼人的对话框,并且允许合并两个以上的文件..
|
4. 制作补丁
|
这次我们还是用Diablo2oo2 Universal Patcher制作补丁, 因为它有个独特的功能就是它能够给加壳的程序制作补丁,更重要的是它支持 AsPack. 最后的补丁应该能够直接用于原始的安装后的文件,而不需要先脱壳再减小发布时的体积. 还有就是我们这次要使用不同的方法制作补丁, 搜索与替换("Search and Replace"). . 搜索与替换("Search and Replace"), 总体构思
这就是 DuP 的搜索与替换("Search and Replace")表:
在1中输入原来的字节格式,在2中输入新的格式, 点击3中的添加(Add)按钮,然后是4中的检查"搜索字节"事件(Check occourrence of "Search Bytes") . 最后应该在5中能见到添加的结果 还得提一下, 通常说来如果字节格式不唯一,DuP允许选择需要修改的事件数量,即使用事件(Occourrence)框.
选择字节格式
A1 081E4B00 MOV EAX,DWORD PTR DS:[4B1E08] 它有一个操作码 A1 (代表MOV EAX, DWORD PTR DS:[]), 操作数 081E4B00 (4B1E08逆序). 考虑到这点, 我们可以继续深入了, 来理解处理方法. 同样是为了简洁,我只列出本文第三部分中的第一处修改. 我只完整的说明如何一步步完成此处修改,对另外几处我只给出最后结果,你可以自己试一下手. 原来的代码: 字节形式是这样的: 75,14,A1,08,1E,4B,00,83,38,00,74,0A,A1,9C,1E,4B,00,83,38,00,75,12,BA,00,72,4A,00 黄色部分代表操作数.
好,那么将这些可能变化的东西从字节格式中去掉,替换成两个星号(*) 75,**,A1,**,**,**,**,83,38,**,74,0A,A1,**,**,**,**,83,38,**,75,**,BA,**,**,**,** 再往深处想想这些操作数,你会发现我们可以将其中的一些留下,比如第一个操作数,是14, 它是一个相对跳转目的地的偏移量(意即向前14个字节),我可以断定即使软件的下一个版本中代码位置发生了变化,这一部分也是作为一个整体而变化位置的,而这14个字节的相对偏移量很小,所以跳转的目的地应该还会在这个相对位置上. 004A712A处的指令操作数是 00, 也可以肯定在未来的版本中不会发生变化. 这样我们就得到了最后要搜索的串: 原来的字节序列:
总结一点就是: 不要将离你要修改的代码太远的偏移量包括进去, 其余的操作数可以留下. 修改后的代码如下:
这次会得到这样的字串:
修改后的字节序列: 好, 将它们输入 Dup
Note: 注意: 如果右击编辑区域1 或 2 会出现一个菜单允许你将要搜索的格式粘贴进去,而不用手动输入(CTRL-C不起作用).
在5中选择你所添加的字节格式再按按钮4得到结果:
长话短说了,另外两个字节格式是这样的: 修改2:
原来的字节序列:
修改3: 我想现在你应该已经能够照葫芦画瓢找到它们了吧.
准备好了要补丁了么, 别忘了选中"目标已被加壳"( "Target is packed" )复选框,
保存工程后生成补丁. |
5.
结论
|
所得到的经验
我意识到每一次想要把道理讲得基础些就得写一大堆东西,抓一大堆图片. 所以越简单的教程就要花费越长的时间,
不管怎样吧,我些这些东西是希望RCE的新一代们能够茁壮成长.. ^__^ |
6.
致谢
|
[MAIN TEAM]
|