23、加壳与脱壳(一):UPX加壳原理、手动脱壳、ESP定律

说到逆向,加壳与脱壳是绕不开的一道坎。我刚开始学逆向那会儿,拿到一个程序,用IDA一打开,全是乱七八糟的导入表,函数名一个都看不到。当时我就懵了——这玩意儿怎么玩?后来才知道,这叫加壳。

UPX是最经典的壳之一。它免费、开源、压缩率高,很多恶意软件也喜欢用它。说白了,UPX干了两件事:压缩代码、隐藏真实入口。你看到的程序,其实是一个"壳程序",真正的代码被压缩藏在后面。

嗯,咱们今天就把UPX扒个底朝天。

UPX加壳原理

UPX的工作原理其实不复杂。它把原始PE文件的代码段和数据段压缩,然后塞到一个新的节区里。程序启动时,先执行壳代码,壳代码负责解压缩,最后跳转到原始入口点(OEP)。

我画了一张流程图,帮你理清这个逻辑:

UPX加壳与脱壳核心流程 原始PE文件 (代码段+数据段) UPX加壳 加壳后PE (壳代码+压缩数据) 运行 壳代码执行 (解压缩 → 修复IAT) 跳转到OEP (原始入口点) 原始代码运行 (正常执行逻辑) 脱壳:找到OEP → dump → 修复

你看,加壳后的程序入口点(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内存:

  1. 插件 → Scylla → 打开
  2. OEP字段填:0040C0C6(你实际的OEP地址)
  3. 点击"IAT Autosearch"自动搜索导入表
  4. 点击"Get Imports"获取导入函数
  5. 点击"Dump"保存为新的exe文件
  6. 点击"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