栈平衡原理定位 OEP

加壳软件必须保证外壳初始化的现场环境(各寄存器的值)与原程序的现场环境是相同的;

加壳后的程序在初始化时保存各寄存器的值,待外壳执行完毕再恢复各寄存器的内容,最后跳转到原程序执行;

通常,加壳后的程序在开始时使用 PUSHAD/PUSHFD 来保护现场,外壳执行完毕后,使用 POPAD/POPFD 恢复现场;

也就是说,加壳软件必须遵守栈平衡原理;

使用工具

  • OllyDbg 1.10原版,简称OD
  • OD 汉化插件均来自互联网;
  • UnPackMe来自互联网,仅供学习使用;
  • 加壳工具为UPX,感谢 🙏 开源;
  • 文中特殊数字均是HEX,为了书写方便采用DEC

分析思路

方法一

  1. 首先,使用 UPX 压缩 CM;

  2. 将加壳后的 CM 导入 OD 后,查看 EP 以及 ESP:

    查看 EP 以及 ESP

    EP 处使用 PUSHAD 备份当前各通用寄存器的值,然后将会在外壳执行完毕后再使用 POPAD 恢复各寄存器的值,以保证程序到达 OEP 时,寄存器的内容保持不变;

    此时 ESP 的值为:0012FFC4;

  3. F7单步执行程序,也就是执行PUSHAD指令后,8 个通用寄存器的内容压入栈:

    通用寄存器的内容压入栈

  4. 加壳程序必须遵守栈平衡原理,所以在执行完毕后,一定会访问栈以恢复各寄存器的内容,所以给处于栈中的任意寄存器内容设置硬件访问断点:

    硬件访问断点

    这里以 EAX 寄存器的内容为例,使用HR 12FFC0,设置硬件访问断点;

    设置完毕后,可以在调试菜单,硬件断点选项下看到已经设置的硬件断点;

  5. 运行程序后, 程序中断:

    程序中断

    中断位置的 POPAD 访问了保存在栈中的寄存器内容,触发了断点;

    同时,各寄存器已经恢复到 EP 时的状态,ESP 重新指向 0012FFC4,说明栈中保存的内容被丢弃了,使用后释放,这就是堆栈平衡;

  6. F7单步执行程序,程序跳转到 OEP:

    OEP

    此时各寄存器的内容以及 ESP 的值与程序处于 EP 时保持一致;

  7. 可以把整个外壳理解为一个函数或子程序,在执行过程中会遵守栈平衡原理,所以当外壳执行完毕跳转到 OEP 时,ESP 的值保持不变;

方法二

  1. 大多数程序 OEP 的第 1 行指令都是压栈指令(PUSH):

    都是压栈指令(PUSH)

  2. 向栈压入数据时,栈指针减小,向低地址移动;从栈中弹出数据时,栈指针增加,向高地址移动;

    向栈压入数据时

    以当前 CM 为例,执行PUSH 0后,ESP 由 0012FFC4 指向 0012FFC0,地址减小了,这是程序到达 OEP 后执行的第 1 条指令;

  3. 反向思考一下,如果给 0012FFC0 设置写入断点,是不是就可以到达 OEP?

    设置硬件写入断点

    将程序载入 OD 后,程序停留在 EP 位置,此时的 ESP 为 0012FFC4,根据栈平衡原理,外壳执行完毕后,ESP 保持不变,所以,OEP 的第 1 条 PUSH 指令,会将内容写入 0012FFC0(向栈压入数据时,栈指针减小,向低地址移动);

    确定写入地址后设置硬件写入断点HW 0012FFC0

  4. 运行程序后,程序中断在代码段:

    程序中断在代码段

    可以看到,由于PUSH 0指令向 0012FFC0 写入内容,从而触发了硬件写入断点,导致中断,而这里就是 OEP;