Stolen-Code 与 Anti-Dump
使用工具
- OllyDbg 1.10原版,简称
OD
; OD
汉化
和插件
均来自互联网;- UnPackMe来自互联网,仅供学习使用;
- Dump 工具为 PETools,来自互联网;
- IAT 重建工具为 ImportREC,来自互联网;
- 区段导出工具为Pupe,来自互联网;
- 区段合并工具为Peditor,来自互联网;
- 文中特殊数字均是
HEX
,为了书写方便采用DEC
;
操作流程
此次使用 KiUserExceptionDispatcher 函数来寻找 OEP,学习一种新技能;
KiUserExceptionDispatcher 函数是用户态异常派发函数,所有异常都会经过该函数来派发,函数的第 2 个参数指向产生异常时的上下文;
将 CM 导入 OD 并运行后,在 log 窗口可以看到很多异常,重点关注最后一次异常:
异常法的原理就是:在异常前有 DLL 调用, 在异常后依然有 DLL 调用,但程序可以正常运行,那么异常前的 DLL 可能是壳调用的,而异常后的 DLL 则是程序运行调用的,所以,异常发生的位置是距离 OEP 最近的位置,此时外壳对代码段的解压操作可能已经完成,那么给代码段设置内存访问断点后,就可以很快到达 OEP;
重载程序,goto 到 KiUserExceptionDispatcher 函数行首,设置条件断点;
其中,表达式为
ESP + 14
,这是因为 KiUserExceptionDispatcher 函数的第 2 参数位于当前领空的 ESP + 14 处;中断条件则是异常的地址为最后一次异常产生的地址,别忘了勾选按条件中断;
运行程序,程序中断后,给代码段设置内存访问断点并运行程序,程序到达 OEP:
当前为 C++ 程序,但入口却不是默认的 OEP,向上滚动代码,根据代码逻辑判断,004271B0 才是程序真正的入口点,而此处却是毫无逻辑的花指令;
C++ 程序的入口点特征:
1
2
3
4
5
6
7
8
9
10
11
12Microsoft Visual C++ 6.0
00496EB8 |. 55 PUSH EBP
00496EB9 |. 8BEC MOV EBP,ESP
00496EBB |. 6A FF PUSH -1
00496EBD |. 68 40375600 PUSH Screensh.00563740
00496EC2 |. 68 8CC74900 PUSH Screensh.0049C78C
00496EC7 |. 64:A1 0000000>MOV EAX,DWORD PTR FS:[0]
00496ECD |. 50 PUSH EAX
00496ECE |. 64:8925 00000>MOV DWORD PTR FS:[0],ESP
00496ED5 |. 83EC 58 SUB ESP,58这里也是今天的重点之一
Stolen-Code
;Stolen-Code
直译过来就是被盗的代码;相同编程语言的程序在编译后 OEP 的风格相似,而某些壳在处理 OEP 的代码时,把这些固定的代码替换(花指令)或 NOP 掉,然后把它们放到壳代码的空间里面(而且还常伴随着花指令),使原程序的开始从壳空间开始,然后再 JMP 回程序空间;
如果脱壳后,这一部分 OEP 代码就会遗失,也就达到了反脱壳的目的,这就是 Stolen-Code 技术,或者更确切的说是 Stolen OEP Code 技术;
了解 Stolen-Code 技术后, 接下来就是找回丢失的 OEP 代码了,离 OEP 最近的壳代码就是最后一次异常产生的位置;
重载程序,在 KiUserExceptionDispatcher 函数的行首设置条件断点,程序中断后,在中断位置所在的区段设置访问断点(F2):
在此处设置断点的目的是跟随到调用位置,也就是离 OEP 最近的位置,断点设置好后,运行程序;
程序中断后,查看代码逻辑,应该就是此处了:
此处很多都是没有实际用途的花指令,为什么要用花指令呢?
因为花指令不会影响程序的正常运行,既然丢失的 OEP 代码夹杂其中,那就只能 F7 单步寻找了;
C++ 程序的 OEP 前两行代码为
PUSH EBP
和MOV EBP, ESP
,这是生成栈帧的指令,目的是为了保持堆栈平衡,而之后的代码很可能因为程序逻辑的不同而不同,所以,前两行代码是关键:功夫不负有心人,终于找到了
PUSH EBP
,那么后面的代码只要是有意义的就是 OEP 中的代码,保存这些代码的指令;运行到此处时,意味着所有丢失的代码已经找到了:
因为这条指令把当前 OEP 的地址 PUSH 到堆栈,那后面肯定还有一个 RETN 用来跳转到当前 OEP;
此处 RETN 后就是跳转到 OEP;
记录丢失代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13003D6A4A 55 PUSH EBP
003D6A6F 8BEC MOV EBP,ESP
003D6A93 6A FF PUSH -1
003D6AB9 68 600E4500 PUSH 450E60
003D6AE3 68 C8924200 PUSH 4292C8
003D6B0D 64:A1 00000000 MOV EAX,DWORD PTR FS:[0]
003D6B35 50 PUSH EAX
003D6B59 64:8925 0000000>MOV DWORD PTR FS:[0],ESP
003D6B85 83C4 A8 ADD ESP,-58
003D6BAC 53 PUSH EBX
003D6BCF 56 PUSH ESI
003D6BF6 57 PUSH EDI
003D6C1B 8965 E8 MOV DWORD PTR SS:[EBP-18],ESP用找到的指令替换当前 OEP 之前的花指令:
找到的指令是:558BEC6AFF68600E450068C892420064A10000000050648925000083C4A85356578965E8;
选择起始行,按下
Ctrl + E
,取消保持大小
选项,将指令粘贴到 16 进制编辑框,然后点击确定完成修改;替换前后对比;
使用 PETools 将内存数据 Dump 并保存为可执行文件;
定位到 IAT 之后发现,虽然 IAT 的数据没有被重定向,但还是有些异常:
IAT 之间不是以 DWORD 的 0 分隔的,而是无效数据;
尝试使用 ImportREC 获取 IAT 数据,发现无效项会干扰正常获取:
Cut thunk(s)
无效选项后,获取正常,然后修复程序;然而,修复后的程序却不能运行,导入 OD :
运行程序后弹出错误弹窗;
打开内存映射窗口,和原程序对比后发现错误地址所在区段在 Dumped 的程序中不存在,这就是引起异常的原因,也是 Anti-Dump;
解决的方法很简单,将程序中缺失的区段补回来不就完了:拷贝原程序的对应区段:
使用 pupe.exe 将原程序的 00AC0000 区段提取出来;
将保存的区段合并到修复程序:
使用 peditor.exe 将提取的区段合并到修复失败的程序中;
还没有完,由于是直接拷贝原程序的区段,当前程序若要正确载入区段,需要修改此区段的 RVA;
区段的 RVA = 区段起始地址 - 基址 = 00AC0000 - 00400000 = 6C0000;
右键菜单编辑节,修改区段 RVA 并保存;
由于修改了区段数据,所以还要重新构建 PE 头文件:
将修复的程序拖入 LoadPE,重新构建 PE;
运行程序正常,导入 OD,跟随任意调用:
一切正常,脱壳完成!