逆向工程核心原理第二十章
"内嵌补丁"练习
"内嵌补丁"是"内嵌代码补丁"(Inline Code Patch)的简称,难以直接修改指定代码时,插入并运行被称为"洞穴代码"(Code Cave)的补丁代码后,对程序打补丁。常用于运行时压缩(或加密处理)而难以直接修改的情况。
- 上图左边描述的是典型的运行时压缩代码(或者加密代码)。EP代码先将加密的OEP代码解密,然后再跳转到OEP处。若要打补丁的代码存在于经过加密的OEP区域是很难打补丁的(即使知道代码所在位置也是如此),因为解码过程中可能会解出完全不同的结果。
- 解决上述问题的简单方法就是,在文件中加一层中间层的补丁代码,让EP代码解密后跳转至中间层。在中间层执行补丁代码后(由于已经解密OEP,所以可以这么修改)再跳转到OEP处。
练习:Patchme
《逆向工程核心原理》作者的github上可以下载本章的patchme程序,https://github.com/reversecore/book
运行程序,可以看到程序非常简单,只有两个弹窗。
这个patchme比较简单,只要修改上面两处字符串即可。
调试
用Ollydbg打开unpackme#1.aC.exe,可以看到EP非常简单
跟踪调试,很快可以看到第一个解密循环,如下图所示,第一个解密循环作用是将"004010F5"~"00401248"(004010F5+154-1)处的数据XOR(异或) 44
知道第一个循环作用后,我们跳过该循环,继续往下调试,很快碰见第二个解密循环,将"00401007"~"00401085"处的数据XOR(异或)7,第二个解密循环的下面是第三个解密循环,第三个解密循环,解密的区域与第一个相同,只是XOR的值为11,说明"004010F5"~"00401249"区域经过了双重XOR加密
跳过第二、三个解密循环后,继续调试,就能发现"00401038"处的校验函数,该函数循环从通"004010F5"~"00401248"区域读取4个字节数据累加得到校验和,再将结果与"31EB8D80"比较,判断程序是否被修改。若校验失败会弹出"CrC of this file has been modified !!!"的错误提示。0040105D处的CALL 0040108A命令是调用另一解密循环,用来解密0040124A~00401279处的数据
EDX寄存器为4个字节大小,像这样向其中不断加上4个字节的值,就会发生溢出(overflow)问题。一般的校验和计算中常常忽略该溢出问题,使用最后一个保存在EDX的值
跳过校验和部分,继续调试,很快就会跳转至OEP处,如下图所示,OEP所在地址为0040121E
如果解密后,Ollydbg在0040121E~00401248没有显示为汇编指令,可以先选中0040121E~00401248这片区域,然后右键选择=>分析=>分析代码,然后Ollydbg就会把该处数据视为汇编代码展示出来
OEP代码用来运行对话框,执行位于0040123E地址处的CALL DialogBoxParamA()命令后即弹出对话框。下面是DialogBoxParamA() API的定义
1
2
3
4
5
6
7INT_PTR WINAPI DialogBoxParamA(
__in_opt HINSTANCE hInstance,
__in LPCTSTR lpTemplateName,
__in_opt HWND hWndParent,
__in_opt DLGPROC lpDialogFunc,
__in LPARAM dwInitParam
)DialogBoxParamA() API的第四个参数lpDialogFunc用来指出Dialog Box Procedure的地址。地址40122C处有条push 4010F5命令,由此可见,函数第四个参数的地址为4010F5(栈先进后出,所以第二个进栈即位倒数第二个出栈)。
下图为004010F5处的代码,可以看到我们要修改的字符串都在里面
根据上面的分析,可以总结出下面的,代码结构图。如图所示,[EP Code]只是用来调用[Decoding Code]的,实际的解密处理是由[Decoding Code]完成的。按照[B]-[A]-[B]的顺序解码,运行解密后的[A]区代码,在[A]区代码会求得[B]区的校验和,并据此判断[B]区是否发生过改变。然后对[C]区解码,最后跳转至OEP处(0040121E)
"内嵌补丁"练习
- 操作顺序:首先向文件合适位置插入用于修改字符串的代码,然后在上图的[A]区域将JMP OEP命令修改为JMP补丁代码(修改时要充分考虑文件中的[A]区域处于加密状态)。在补丁代码中更改字符串,然后再通过JMP命令跳转至OEP处,这样整个内嵌补丁过程就完成了。
补丁代码要设置在何处
这个问题在进行内嵌补丁的过程中非常重要。有如下3种设置方法:
- 设置到文件的空白区域
- 扩展最后节区后设置
- 添加新节区后设置
补丁代码较少时,使用方法1,其他情况使用方法2或方法3。首先尝试方法1,使用PE View查看示例文件的第一个节区(.text)头,如下图所示
接着查看Section Alignment的值
可以看到第一个节区Virtual Size为280,但是由于Section Alignment为1000,所以第一个节区后面RVA 1280之后存在大量NULL填充的空白区域,我们可以在该区域添加补丁代码。
用Ollydbg查看00401280地址,如下图所示,可以看到,该处之后确实存在大量空白区域
我们开始添加补丁代码,首先添加我们需要修改的字符串。鼠标选中00401280地址处,右键选择二进制编辑
取消勾选保持大小,然后在ASCII中输入所需字符串,这里为"blog.iz4.cc",然后点确定即可
同样的在0040128C处添加字符串"Unpacked!"
如果添加字符串后,该部分数据被Ollydbg识别为汇编指令的话,可以鼠标选中该区域,右键选择 分析=>分析代码,然后OD就能将该部分识别为ASCII字符串了。
添加好字符串后,接着我们添加补丁代码的汇编指令,在00401296处按空格即可编辑汇编指令
1
2
3
4
5
6
7
8
9
10
11
12
13mov ecx, 0xC ; 0xC为要拷贝的字符串的字符长度(包括'\0')==> blog.iz4.cc\0
mov esi, 00401280 ; 让ESI指向第一个需要拷贝的字符串所在地址
mov edi, 00401123 ; 修改的目标字符串所在地址
rep movsb ; <==> rep movs byte ptr es:[edi],byte ptr ds:[esi]
;这个指令意思就是将ESI指向的地址的值以1字节方式拷贝到EDI指向的地址中
;重复执行ECX次,每次执行后ESI+1,EDI+1,ECX-1
mov ecx, 0xA ; 0xA为要拷贝的字符串的字符串长度(包括'\0')==> Unpacked\0
mov esi, 0040128C ; 让ESI指向第二个需要拷贝的字符串所在地址
mov edi, 0040110A ; 修改的目标字符串所在地址
rep movsb ; <==> rep movs byte ptr es:[edi],byte ptr ds:[esi] ; 作用同上
jmp 0040121E ; 跳转至OEP添加完,指令后,我们先修改地址00401083处的jmp命令跳转至我们的补丁代码位置,测试效果
如下图所示,可以看到,两个字符串成功被修改,接下来就是保存修改到可执行文件
在左下角的数据窗口中,选中修改的00401280~004012BC区域,然后右键选择复制到可执行文件,再保存即可
这里不直接保存00401083处的jmp命令修改,因为该区域是加密的,所以文件中修改该处的jmp指令也应该替换为加密后的数据
由00401083 => RVA 1083 => RAW: 483,用HEdit查看该处偏移。从文件偏移看加密只到485,后面的0000并不是加密区域。
如下图所示,OD中
jmp 00401296
对应的机器码为E90E020000,从上面可知,后面两个00不用加密。因为是XOR(异或)加密,所以加解密都是对同一个key进行XOR运算即可。所以应该将"E90E02"加密后为"EE0905",所以文件483偏移处的"EE9106"应该修改为"EE0905"1
2
3E9 xor 07 = EE
0E xor 07 = 09
02 xor 07 = 05最后使用HEdit将文件483偏移处的"EE9106"应该修改为"EE0905"保存即可,运行可以发现程序成功跳转至补丁代码。