【中文译者:月中人 时间:2006-7-23 英文原文:看雪论坛-外文翻译区-HookInLinePatching.pdf】
通过挂钩API函数
给加了保护的应用程序内嵌补丁
原著:SUB Z3R0
主题:Spirit Of The Wind
论坛:Seek'n Destroy TeaM 2oo6
目 录
壳的原理和内嵌补丁的原理 ……………………………………………2
简易的挂钩方法 …………………………………………………………3
较难的挂钩方法 …………………………………………………………4
关于我的方法 ……………………………………………………………5
用挂钩方法做ASProtect 2.xx 壳的内嵌补丁 ……………………… 6
用挂钩方法做tElock 0.98+ 壳的内嵌补丁 ………………………… 8
用挂钩方法做Armadillo 4.xx 壳的内嵌补丁 ……………………… 10
用挂钩方法做Execryptor 2.xx 壳的内嵌补丁 ………………………12
用挂钩方法做TheMida 1.xx壳的内嵌补丁 ……………………………15
介绍:
要了解内嵌补丁的含义必须弄明白下面这些问题:
问题:一个加壳程序或加保护程序通常对原程序做了什么?
回答:加壳程序把应用程序原始的代码压缩(如Aspack、Upx壳)或者加密(如tElock、TheMida壳)或者兼有二者(如Asprotector、Armadillo壳),然后把他自己的代码添加到结果文件,最后把EP(入口点)改成他自已代码的EP。
问题:加壳程序的代码有些什么操作?
回答:1. 给自己分配一些内存(也许没有这一步)
2. 解密出他的代码并写入分配的内存(自我解密)
3. 解密出程序原始的代码并写入原始的映像基址处(通常是400000h)
4. 有些壳还要去除API函数【输入表】、或者模拟API函数的功能。
5. 修复输入表(或重定向对API函数的调用到模拟API函数的地址)
6. 跳转到OEP(原始入口点)
注意:第二步可能发生两次以上,这使得正常的内嵌补丁更难做。现在你了解壳的原理,接下来要了解内嵌补丁的原理,问题是类似的:
问题:什么是内嵌补丁?
回答:内嵌补丁是一种不需要先把一个加了壳的程序脱壳就能打补丁的技巧!
问题:它是怎么做到的?
回答:为了做到这一点,我们必须在壳代码写完程序原始的代码之后、还没运行到我们想要修改的那部分应用之前,插入一段代码。内嵌补丁最重要的问题之一就是获取CRC值,通常可以用API函数(CreateFile - ReadFile、CreateFileMapping - MapViewOfFile)读文件得到它。
(想了解更多,请看Lena教程和ARTeam初学者Ollydebug教程)
(想学习内嵌补丁的高级技巧,请看JohnWho的ARMA/ASPR内嵌补丁教程)
问题:我们怎样挂钩一个API函数呢?
回答:我知道有两个方法:
简易方法:
1. 读取API函数开头的5字节或6字节,保存于某地址X处。
2. 把API函数开头的5字节或6字节改写为一个跳转指令【注】,跳到地址X。
3. 在X+5或X+6这个地址开始写我们的代码。
4. 代码最后写入一个跳转指令,跳到这个地址——“API函数地址”加5或加6。
【原注】可以使用下面这些指令实现跳转:jmp [xxxx] (6 byte) 或 jmp xxxx (5 byte) 或 push xxxx ret (6 byte)
我的函数的任务:把[EAX]处5字节写到[EBX];把“跳转到[EBX]”这条指令写到[EAX];把“跳转到[EAX+5]”这条指令写到[EBX+10]。
[图1]
这个方法是对的,但不适用于所有的API函数以及所有的Windows平台。
较难的方法:
1. 读取API函数开头5、6字节并保存好。
2. 把API函数开头5、6字节改写成一个跳转到地址X的指令。【可以是PUSH/RET方式】
3. 在地址X那里写入我们的代码,接着写入一些代码(功能是:先保存API函数原始的返回地址,再把API函数的返回地址改为我们代码中的地址Y),再还原API函数开头5字节或6字节,最后写入一个跳转指令跳到API函数。
4. 地址Y那里的代码必须再一次执行第2步,然后跳转到原始的返回地址。
[图2]
【第1,2步是在程序原始入口点完成,第3步在截获时执行,它把API变回原样了,所以第4步必须再一次做第2步即Install
Hook Again,以便下次截获。API函数的原始返回地址是在每次截获时读自DWORD
PTR SS:[ESP]】
这个方法较难,但它适用于所有的API函数以及所有的Windows平台。
回答:我的方法是教你用挂钩截获API函数以挫败CRC检查以及给程序打补丁(在程序的原始代码已被解密或解压出之后)。
回答:我做内嵌补丁的方法分为以下几个步骤:
第一步:用以下技巧之一挫败CRC检查:
a) 挂钩ReadFile或MapViewOfFile,把原始字节重新写回缓冲区。(对于Asprotector壳我们用这个办法)
【缓冲区指从MapViewOfFile返回时EAX+128处】
b) 挂钩最近一次做CRC检查的API函数调用,把它计算出来的CRC值改写成程序原始的CRC值。(我们常用这个办法)
c) 对于有内存检查的壳,我们首先应该改正内存数据。(对于有TheMida保护的目标程序我们用这个办法)
第二步:挂钩如GetModuleHandleA(对于Delphi/C++程序)、GetVersion (用于C++程序)、ThunRTMain(用于VB程序)等API函数,它们会在到达OEP后不久被调用;打上补丁。(有些案例中,我们必须用计数器数它的调用次数来接近OEP)
(如果加壳程序没有去除输入表,你也可以通过比较GetModuleHandleA的返回地址,如果返回地址小于“代码段首地址+代码段大小”之和,你就能知道,这次调用是来自程序的原始代码,也就是到打补丁的时候了。)
【"代码段"指程序原始代码】
现在用一些目标程序实践一下:
JohnWho InLineMe :
1, ASProtect
2.11
sND!
2oo5 UnPackMes :
2, TELock 0.98+
3, Armadillo 4.20
5, TheMida 1.3.x
6, ExECryptor
2.1.70
用挂钩方法做ASProtect 2.xx壳的内嵌补丁:
用ToPo打开我们的目标程序(JohnWho InLineMe)…
[图3]
【计有815字节是闲置的,可以在这里写补丁代码】
有815字节足够了!所以我们将使用已有的节段 … 空闲内存地址是4898D1 … 所以我们把程序EP改为偏移地址898D1。
想想,在这个案例中我们必须做点什么:
1. 空闲内存会被ASPR改变,所以我们得分配一些内存用于安装我们的钩子。
2. 在分配的内存中写入我们的代码并修正地址。【即代码重定位】
[图4]
3. 挂钩MapViewOfFile函数以挫败CRC:
我们将挂钩MapViewOfFile … 修改AccessMode为1 ( File_Map_Copy ) (使之变为可以修改的) … 修改MapViewOfFile函数的返回地址,使之返回时来到我们的代码(执行过MapViewOfFile函数之后,我们就修补它的缓冲区——缓冲区地址放在EAX) …
然后我们比较一下文件映像的EP和我们的EP(898D1),把文件映像的EP重写为壳的OEP(1000)以修正CRC。
4. 挂钩GetModuleHandle函数以去掉nag屏:
第一次以NULL为参数的GetModuleHandle调用是来自代码段,指的是应用程序的原始代码 … 这意味着程序在内存里已经被脱壳了,正是打补丁的好时机。[图5]
运行程序 … 好!烦人的东西去掉了!
先把文件备一份,再用ToPo打开我们的目标(用SnD UnpackMe吧?我们要修补它的标题)
…
给文件增加一个400字节的新节 …
用Olly打开原始文件备份 … 下CloseHandle函数参考断点
… 运行并等到olly断下
… 按Ctrl+F9来到Ret … 再按F8看代码:
[图6]
在4659DE处下断点,运行 … 抄下EAX的值(这是原始的CRC) …
现在想想对于此案例我们需要做点什么:
1. tElock壳保护的应用程序他们的输入表没有GetProcAddress … 所以我们只好从kernel32的输出表找该函数的地址(需要一点专业技术!) … 你也可以在他们的输入表中手工增添GetProcAddress或者用LordPE也行 …
2. 从上面的图片见到,我们可以在第一次调用CloseHandle的返回地址那里写入"mov eax,04e468f32 | jmp 4659DE"来通过CRC检查。
3. 挂钩GetModuleHandle来打补丁:
第一次以NULL为参数的GetModuleHandle调用是来自代码段,指的是应用程序的原始代码 …
这意味着程序在内存里已经被脱壳了,正是打补丁的好时机。
这段代码先找到GetProcAddress,再挂钩CloseHandle和GetModuleHandle …
[图7]
挂钩回调函数 …
[图8]
现在运行它
[图9]
呵呵!标题被修改了 … 啊!如果让teddy知道,他会杀了我的!
我确信这个方法同样适用于hmemcpy2(但我还没时间测试!)
用olly打开原始文件备份 … 下OutputDebugStringA函数参考断点 … 运行程序第2次在这个函数断点停下 … 按Ctrl+F9来到Ret … 然后按F8看代码:
[图10]
在堆栈窗口点鼠标右键,到菜单项“地址”下选择“相对于EBP” … 现在堆栈窗口是跟随EBP:
[图11]
图中显示出5个CRC值 … 都抄下来 … 后面做内嵌补丁要用到它们。
(你也可以用Armadillo
CRC Finder 1.3 By AvAtAr找到这些CRC)
现在该想想我们的目的:
1. 挂钩OutputDebugStringA,用计数器检测到第二次调用,重写那些CRC。
2. 挂钩GetModuleHandle,用计数器检测到第5次调用,因为这一次的参数是NULL,也就是说调用源靠近OEP。
[图12]
现在运行程序看它的标题:
[图13]
[图14]
用挂钩方法做Execryptor 2.xx壳的内嵌补丁:
用ToPo打开我们的目标(还是用SnD UnpackMe吧?我们要再一次修补它的标题)
…
给文件增加一个300字节的新节 …
在本案例是在到达EP之前检查CRC!它是在TLS-CallBack计算 …
所以修改TLSCallBack到我们添加的代码。
我们得先找到Execryptor那些CRC值:
设置第一次暂停于系统断点如图所示
[图15]
用olly打开原始文件备份(我确信你已经备了一份!) … 下_lread函数参考断点 … 按Shift+F9键14次(你那里可能不一样吧?我的意思是最后一次_lread调用) … 按Ctrl+F9来到Ret … 然后按F8看代码:
[图16]
在堆栈窗口点鼠标右键,到菜单项“地址”下选择“相对于EBP” … 现在堆栈窗口跟随EBP:
[图17]
抄下这些CRC值。
现在我们要做什么呢?
1. 挂钩_lread函数,用计数器检测到第14次调用时重写那些CRC值。
2. 挂钩GetModuleHandle函数给消息框标题打补丁:
第一次以NULL为参数的GetModuleHandle调用是来自代码段,指的是应用程序的原始代码 …
这意味着打补丁的好时机来了。
请看代码和注释加深理解:
[图18]
[图19]
运行程序看看:
[图20]
用ToPo打开我们的目标(SnD UnpackMe) …
给文件增加一个550字节的新节(我曾经增加.themida节尺寸并使用它) …
修改EP为新增加空间的偏移地址,TheMida壳没有任何CRC检查(这让我们的工作轻松不少),但是它有少许内存检查!
但是还有一些更厉害的难题!它的输入表中没有GetModuleHandle和GetProcAddress!
现在我们能做什么?
1. 我们探索一下PEB_LDR_DATA就可以取得kernel32的ImageBase值!然后你可以通过kernel32的输出表获取GetModuleHandle和GetProcAddress函数地址(逆向是家常便饭!这个会难一点了?)。或者你就干脆(手工或用LoadPE)把需要用的API函数添加到程序的输入表。
2. 首先在内存里复原PE头中的EP!(以便通过内存检查)。
3. 挂钩GetModuleHandle给消息框标题打补丁:
第一次发现GetModuleHandle函数调用的返回地址值小于10000000h时就说明这次调用来自代码节段,所以这时就应该打补丁了。
请看下一页图片加深理解—〉
(注意:在我的案例中我没有向我的文件添加任何节 … 我只是增加.Themida节尺寸以获取空闲空间为我所用
… 所以下图中你会看到我分配一些内存以安装我的钩子(因为themida壳代码会试图在那里写东西))
[图21]
[图22]
【安装好挂钩、还原壳的EP和壳节尺寸之后跳到壳的入口】
[图23]
我想5个例子够了!本文就写到这吧!(还有问题或疑问?我会在SND论坛尽可能地回答。)
感谢:
Teddy Rogers(感谢提供UnpackMes及帮我发布本文)
JohnWho(感谢他漂亮的CRC过关技巧)
致意:
Soda + Sina
, Joker + LordXp , Magic , 和所有 SnD /
Shabgard / UnReal 朋友们。
SUB Z3R0 | Spirit Of The Wind
| - SnD TeaM 2oo6
SnD TeaM
2oo6 论坛 -> http://www.tuts4you.com/forum/