23、加壳与脱壳(一):UPX加壳原理、手动脱壳、ESP定律
说到逆向,加壳与脱壳是绕不开的一道坎。我刚开始学逆向那会儿,拿到一个程序,用IDA一打开,全是乱七八糟的导入表,函数名一个都看不到。当时我就懵了——这玩意儿怎么玩?后来才知道,这叫加壳。
UPX是最经典的壳之一。它免费、开源、压缩率高,很多恶意软件也喜欢用它。说白了,UPX干了两件事:压缩代码、隐藏真实入口。你看到的程序,其实是一个"壳程序",真正的代码被压缩藏在后面。
嗯,咱们今天就把UPX扒个底朝天。
UPX加壳原理
UPX的工作原理其实不复杂。它把原始PE文件的代码段和数据段压缩,然后塞到一个新的节区里。程序启动时,先执行壳代码,壳代码负责解压缩,最后跳转到原始入口点(OEP)。
我画了一张流程图,帮你理清这个逻辑:
你看,加壳后的程序入口点(EP)指向的是壳代码,不是原始代码。脱壳的核心任务就是:找到原始入口点(OEP),把内存中已经解压的原始程序dump出来,再修复导入表。
手动脱壳:ESP定律
ESP定律是手动脱壳最经典的方法。为什么叫"定律"?因为它几乎适用于所有基于pushad/popad的壳。UPX正好就是这种。
原理其实很简单:壳代码开始时会保存寄存器状态(pushad),结束时恢复(popad)。你想想看,pushad把ESP压栈,popad又把ESP弹出来。如果我们能在pushad之后,在栈上下硬件断点,那程序执行到popad时就会断下来。这时候,离OEP就不远了。
核心思路:ESP定律 = 在pushad后,对ESP值设硬件断点 → 断在popad处 → 单步到OEP
实战:用x64dbg脱UPX壳
我拿一个UPX加壳的notepad.exe做演示。你跟着我一步步来。
第一步:加载程序
用x64dbg打开加壳后的notepad.exe。你会看到入口点长这样:
0040D000 > 60 pushad ; 保存所有寄存器
0040D001 BE 00F04000 mov esi, 0040F000
0040D006 8DBE 0000F0FF lea edi, [esi-0x100000]
...
看到那个pushad了吗?这就是我们的突破口。
第二步:设ESP硬件断点
在pushad执行后,ESP的值会变成栈顶地址。我们记下这个值:
ESP = 0019FF64 ; 你的机器上可能不同
在x64dbg的"命令"栏输入:
hr 0019FF64 ; hr = hardware breakpoint on read/write
或者直接在ESP寄存器上右键 → "在内存中跟随" → 然后设硬件访问断点。
小技巧:我个人习惯用hr命令,比鼠标点来点去快。你可以在命令栏直接输入hr esp,x64dbg会自动解析ESP的当前值。
第三步:运行,断在popad处
按F9运行程序。程序会断在popad指令处:
0040D0B0 61 popad ; 恢复寄存器
0040D0B1 - E9 10F0FFFF jmp 0040C0C6 ; 跳转到OEP
看到了吗?popad后面紧跟着一个jmp。这个jmp就是跳向OEP的。
第四步:单步到OEP
按F8单步执行jmp,你就到了OEP:
0040C0C6 6A 70 push 0x70
0040C0C8 68 98180001 push 0x01001898
0040C0CD E8 BF010000 call 0040C291 ; 这才是真正的入口
嗯,这里要注意:OEP处的代码看起来应该是正常的API调用和逻辑,而不是pushad/popad这种壳特征代码。
第五步:Dump内存
在OEP处,用x64dbg的"Scylla"插件dump内存:
- 插件 → Scylla → 打开
- OEP字段填:
0040C0C6(你实际的OEP地址) - 点击"IAT Autosearch"自动搜索导入表
- 点击"Get Imports"获取导入函数
- 点击"Dump"保存为新的exe文件
- 点击"Fix Dump"修复dump出来的文件
注意:我曾经有一次dump完直接运行,结果程序崩溃了。后来发现是OEP地址填错了。记住:OEP是原始入口点的地址,不是壳入口点。另外,如果IAT Autosearch找不到导入表,可以手动指定RVA和大小。
ESP定律的变种与扩展
ESP定律虽然好用,但不是万能的。我遇到过一些UPX变种,它们会在pushad之前修改ESP,或者在popad之后加一些花指令。这时候怎么办?
| 情况 | 处理方法 |
|---|---|
| pushad后ESP被修改 | 用pushfd/popfd代替,或者跟踪栈操作 |
| popad后有多层jmp | 单步跟踪,直到看到正常的函数调用 |
| 壳代码有反调试 | 用x64dbg的隐藏插件,或者手动patch |
| OEP不在popad后直接jmp | 在popad处设断点,然后看栈回溯 |
避坑指南:我曾经遇到一个UPX加壳的程序,popad后面跟了三个jmp,最后才到OEP。当时我差点以为ESP定律失效了。其实只要耐心单步,总能找到OEP。记住:壳代码最终一定要跳到OEP,这是它的使命。
为什么ESP定律有效?
你可能会问:为什么ESP定律能通用?说白了,因为pushad/popad这个模式太常见了。壳代码为了不破坏原始程序的寄存器状态,必须保存和恢复。而ESP在pushad后指向栈顶,这个值在popad之前不会被改变(除非壳代码故意搞鬼)。
我个人的理解是:ESP定律抓住了壳代码的"七寸"。它不需要你理解壳代码的复杂逻辑,只需要利用一个不变的规律——寄存器状态必须恢复。这就像警察蹲点,你不需要知道小偷怎么偷东西,只需要知道他一定会从那个门出来。
总结
UPX加壳的原理就是压缩+隐藏入口。手动脱壳的核心是ESP定律:在pushad后对ESP设硬件断点,断在popad处,然后单步到OEP。最后用Scylla dump并修复。
嗯,这一章的内容就到这里。记住:ESP定律是手动脱壳的入门,但不是终点。后面还有更复杂的壳等着你。不过有了这个基础,你已经可以对付80%的UPX加壳程序了。
公众号:蓝海资料掘金营,微信deep3321