Stolen-Code 与 Anti-Dump

使用工具

  • OllyDbg 1.10原版,简称OD
  • OD 汉化插件均来自互联网;
  • UnPackMe来自互联网,仅供学习使用;
  • Dump 工具为 PETools,来自互联网;
  • IAT 重建工具为 ImportREC,来自互联网;
  • 区段导出工具为Pupe,来自互联网;
  • 区段合并工具为Peditor,来自互联网;
  • 文中特殊数字均是HEX,为了书写方便采用DEC

操作流程

  1. 此次使用 KiUserExceptionDispatcher 函数来寻找 OEP,学习一种新技能;

  2. KiUserExceptionDispatcher 函数是用户态异常派发函数,所有异常都会经过该函数来派发,函数的第 2 个参数指向产生异常时的上下文;

  3. 将 CM 导入 OD 并运行后,在 log 窗口可以看到很多异常,重点关注最后一次异常:

    log

    异常法的原理就是:在异常前有 DLL 调用, 在异常后依然有 DLL 调用,但程序可以正常运行,那么异常前的 DLL 可能是壳调用的,而异常后的 DLL 则是程序运行调用的,所以,异常发生的位置是距离 OEP 最近的位置,此时外壳对代码段的解压操作可能已经完成,那么给代码段设置内存访问断点后,就可以很快到达 OEP;

  4. 重载程序,goto 到 KiUserExceptionDispatcher 函数行首,设置条件断点;

    条件断点

    其中,表达式为ESP + 14,这是因为 KiUserExceptionDispatcher 函数的第 2 参数位于当前领空的 ESP + 14 处;

    中断条件则是异常的地址为最后一次异常产生的地址,别忘了勾选按条件中断;

  5. 运行程序,程序中断后,给代码段设置内存访问断点并运行程序,程序到达 OEP:

    假 OEP

    当前为 C++ 程序,但入口却不是默认的 OEP,向上滚动代码,根据代码逻辑判断,004271B0 才是程序真正的入口点,而此处却是毫无逻辑的花指令;

    C++ 程序的入口点特征:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Microsoft 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

  6. 这里也是今天的重点之一Stolen-Code

    Stolen-Code直译过来就是被盗的代码;

    相同编程语言的程序在编译后 OEP 的风格相似,而某些壳在处理 OEP 的代码时,把这些固定的代码替换(花指令)或 NOP 掉,然后把它们放到壳代码的空间里面(而且还常伴随着花指令),使原程序的开始从壳空间开始,然后再 JMP 回程序空间;

    如果脱壳后,这一部分 OEP 代码就会遗失,也就达到了反脱壳的目的,这就是 Stolen-Code 技术,或者更确切的说是 Stolen OEP Code 技术;

    了解 Stolen-Code 技术后, 接下来就是找回丢失的 OEP 代码了,离 OEP 最近的壳代码就是最后一次异常产生的位置;

  7. 重载程序,在 KiUserExceptionDispatcher 函数的行首设置条件断点,程序中断后,在中断位置所在的区段设置访问断点(F2):

    F2 断点

    在此处设置断点的目的是跟随到调用位置,也就是离 OEP 最近的位置,断点设置好后,运行程序;

  8. 程序中断后,查看代码逻辑,应该就是此处了:

    花指令

    此处很多都是没有实际用途的花指令,为什么要用花指令呢?

    因为花指令不会影响程序的正常运行,既然丢失的 OEP 代码夹杂其中,那就只能 F7 单步寻找了;

  9. C++ 程序的 OEP 前两行代码为PUSH EBPMOV EBP, ESP,这是生成栈帧的指令,目的是为了保持堆栈平衡,而之后的代码很可能因为程序逻辑的不同而不同,所以,前两行代码是关键:

    PUSH EBP

    功夫不负有心人,终于找到了PUSH EBP,那么后面的代码只要是有意义的就是 OEP 中的代码,保存这些代码的指令;

  10. 运行到此处时,意味着所有丢失的代码已经找到了:

    跳转到 OEP

    因为这条指令把当前 OEP 的地址 PUSH 到堆栈,那后面肯定还有一个 RETN 用来跳转到当前 OEP;

    RETN

    此处 RETN 后就是跳转到 OEP;

    记录丢失代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    003D6A4A    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
  11. 用找到的指令替换当前 OEP 之前的花指令:

    找到的指令是:558BEC6AFF68600E450068C892420064A10000000050648925000083C4A85356578965E8;

    替换

    选择起始行,按下Ctrl + E,取消保持大小选项,将指令粘贴到 16 进制编辑框,然后点击确定完成修改;

    替换后

    替换前后对比;

  12. 使用 PETools 将内存数据 Dump 并保存为可执行文件;

  13. 定位到 IAT 之后发现,虽然 IAT 的数据没有被重定向,但还是有些异常:

    IAT

    IAT 之间不是以 DWORD 的 0 分隔的,而是无效数据;

    尝试使用 ImportREC 获取 IAT 数据,发现无效项会干扰正常获取:

    干扰

    Cut thunk(s)无效选项后,获取正常,然后修复程序;

  14. 然而,修复后的程序却不能运行,导入 OD :

    再次导入 OD

    运行程序后弹出错误弹窗;

    区段丢失

    打开内存映射窗口,和原程序对比后发现错误地址所在区段在 Dumped 的程序中不存在,这就是引起异常的原因,也是 Anti-Dump;

  15. 解决的方法很简单,将程序中缺失的区段补回来不就完了:拷贝原程序的对应区段:

    拷贝原始区段

    使用 pupe.exe 将原程序的 00AC0000 区段提取出来;

  16. 将保存的区段合并到修复程序:

    添加

    使用 peditor.exe 将提取的区段合并到修复失败的程序中;

    添加完成

    还没有完,由于是直接拷贝原程序的区段,当前程序若要正确载入区段,需要修改此区段的 RVA;

    区段的 RVA = 区段起始地址 - 基址 = 00AC0000 - 00400000 = 6C0000;

    修改 RVA

    右键菜单编辑节,修改区段 RVA 并保存;

  17. 由于修改了区段数据,所以还要重新构建 PE 头文件:

    重建 PE

    将修复的程序拖入 LoadPE,重新构建 PE;

  18. 运行程序正常,导入 OD,跟随任意调用:

    正常

    一切正常,脱壳完成!