第 25 章 壳与脱壳原理(Packer / Unpacking)
壳(Packer)是逆向中最常见的"障碍物":把程序压缩/加密,运行时在内存中还原再执行。看懂壳的原理,脱壳就变成"在正确的时间点 dump 正确的内存"。
📍 知识点地图 | 主题:壳与脱壳 | 前置:第4-6章 | 后续:第40/47章 | 核心概念:壳分类、OEP、ESP定律、IAT修复
25.1 壳的本质(一句话)
壳 = 一个"前置执行器"
原程序代码被压缩/加密存放
运行时: 壳先执行 → 把原代码还原到内存 → 跳转到原入口(OEP)
分析难点: 你看到的文件里没有原逻辑,只有壳
25.2 壳的分类学
| 类型 | 原理 | 代表 | 脱壳难度 |
|---|---|---|---|
| 压缩壳 | 仅压缩,无对抗 | UPX、ASPack、Petite | ★(upx -d 一键) |
| 加密壳 | 加密+简单反调试 | 常见商业壳低配版 | ★★ |
| 虚拟化壳 | 代码转成 VM 指令 | VMProtect、Themida | ★★★★★ |
| 代码混淆壳 | 控制流/数据混淆 | 各类保护增强 | ★★★★ |
| 移动端壳 | 加密 DEX | 360/腾讯/梆梆(第 10 章) | ★★★ |
25.3 加壳程序的执行架构
文件态:
┌─────────────────────────────┐
│ 壳段(解密器 + 壳代码) │
│ 原代码(压缩/加密的字节) │
└─────────────────────────────┘
↓ 运行
内存态:
┌─────────────────────────────┐
│ 壳执行: 解密/解压 → 写入内存 │
│ 原代码还原到原地址 │
│ 跳到 OEP (Original Entry Point) │
│ 之后 = 正常程序执行 │
└─────────────────────────────┘
关键概念:
OEP: 原程序的真正入口(壳跳向它)
IAT: 导入地址表(壳常加密/隐藏,需要还原才能看导入)
dump 时机: 原代码已在内存且完整、壳已交权 → dump 出来的就是"裸程序"
25.4 壳的识别(第一件事)
| 方法 | 命令/工具 |
|---|---|
| 节区名异常 | 段名不像 .text/.rdata(如 UPX0/UPX1/.vmp0/.themida) |
| 熵值高 | 高熵节区 = 加密/压缩(binwalk -E、DIE 显示) |
| 入口点异常 | 入口不是 CRT 启动,而是壳 stub(入口在最后一个节) |
| 导入很少/异常 | 只有 GetProcAddress/LoadLibrary(动态解析) |
| 检测工具 | Detect It Easy (DIE)、Exeinfo PE、pe-sieve |
rabin2 -S sample # 看节区名(UPX0/UPX1 等)
rabin2 -I sample # 入口点位置
# DIE: 拖进 GUI 直接显示壳名和版本(第一选择)
25.5 脱壳方法论(架构)
策略 1: 官方/通用脱壳器(最省事)
upx -d sample # UPX 直接脱
de4dot (NET) / 移动端专用脱壳工具
适用: 无自定义修改的标准壳
策略 2: 动态 dump(最通用)
1. 调试器/模拟环境加载到 OEP(或 OEP 之后)
2. 用 Scylla / pe dump / 脚本 dump 进程内存
3. 修复 IAT(壳隐藏了导入表)
4. 得到可分析的裸文件 → 扔进 IDA/Ghidra
策略 3: 内存 dump(无需调试器)
Frida: 在壳解密完成后 dump 目标内存区域
适用: 无法调试的移动端/混淆环境(第 10 章)
策略 4: 模拟执行
Unicorn: 跟踪解密循环,hook 解密函数,dump 解密后的代码段
Qiling: 整体模拟运行,在 OEP 断点 dump
适用: 反调试顽固的样本
策略 5: 全程跟踪还原(最难)
记录解密逻辑 → 重放生成解密文件(等价于解压算法的逆向)
适用: 壳做了磁盘级加密(dump 内存也行不通)
25.6 动态 dump 的标准步骤(策略 2 详解)
1. 调试器(x64dbg)打开样本
2. 处理反调试(ScyllaHide 插件一键)
3. 找 OEP 的方法:
a. 断在 VirtualAlloc/VirtualProtect 后的写入完成点
b. 硬件断点法(对 OEP 地址下断——需先猜)
c. ESP 定律: 入口第一条压栈指令后,在栈顶下断点 → 壳转交时命中
d. 单步跟踪直到看到典型 CRT 启动序列(push ebp/mov ebp,esp 等)
4. dump: x64dbg → Scylla → Dump
5. 修复 IAT: Scylla → IAT Autosearch → Fix Dump
6. 验证: 脱壳后文件能在 IDA 中正常分析、导入表完整
ESP 定律(最快的 OEP 寻找法)
原理: 壳入口通常以 pushad/push reg 开始,退出时 popad/pop reg 恢复
方法:
1. 停在入口,记下 ESP 值
2. 对 ESP 指向的栈地址下硬件访问断点
3. 运行 → 断在壳交权点附近 → 附近就是 OEP
适用: 大多数传统壳
25.7 IAT 修复(脱壳后必做的收尾)
为什么: 壳把导入表加密/压缩,dump 的内存里导入不完整
症状: 脱壳文件在 IDA 里导入缺失/无意义
修复:
1. Scylla(x64dbg 配套): IAT Autosearch → Get Imports → Fix Dump
2. 手动: 找到 IAT 起始/大小,Scylla 填上后重建
3. 验证: 对比脱壳前后调用链(原来的 call [0x...] 现在指向正确 API)
25.8 虚拟化壳(VMProtect / Themida)的特别处理
识别: .vmp0/.vmp1/.themida 节区、熵 > 7.5、巨大体积
本质: 原代码被翻译成自定义 VM 指令(不是简单加密!)
普通 dump 无用(内存里也是 VM 字节码)
对策:
1. 动态跟踪: Hook VM handler 分派 → 记录 opcode+操作数 → 还原执行流
2. 符号执行: 对 VM 循环符号化,求解等价逻辑
3. 静态还原: 逆向 VM 解释器 → 写反编译器(第 16 章 VM 逆向)
4. 现实建议: CTF 中优先动态观察"程序做了什么"而非完整还原 VM
关注: 它访问了什么数据/调用了什么 API → 行为级理解
25.9 移动端壳(Android DEX 加壳)
形态: classes.dex 被壳加密,运行时由壳加载器解密真实 dex
流程:
1. 识别壳: jadx 反编译只有壳代码 + 壳特征(com.secneo/com.stub 等)
2. 运行时 dump: Frida Hook DexClassLoader/ClassLoader.loadClass
→ 读取 BaseDexClassLoader 的 pathList.dexElements
→ 逐个 dump 内存 dex
3. 修复 dex(checksum/sha1)→ jadx 分析
4. 或专用脱壳: FART / Youpk / 黑盒脱壳机
25.10 脱壳检查清单
□ 确认壳类型(DIE/节区/入口/导入特征)
□ 优先试官方脱壳器
□ 需要时绕反调试(ScyllaHide/环境改造)
□ 定位 OEP(ESP 定律/断点法)
□ dump 内存(调试器/Scylla/Frida/Unicorn)
□ 修复 IAT(Scylla)
□ 验证: IDA 打开脱壳文件,导入完整、函数可读
□ 虚拟化壳: 接受"行为级理解"而非完整还原
动手练习
- 用 UPX 给一个程序加壳再脱壳(
upx sample && upx -d sample),对比脱壳前后节区和入口点。 - 拿一个 UPX 壳样本,不用
upx -d,练习手动 ESP 定律找到 OEP 并 dump。 - 用 Scylla 修复一个 dump 后的 IAT,验证导入表完整。
- 在隔离环境用 Frida dump 一个加壳 APK 的真实 dex(第 10 章配合)。
深入阅读
- 仓库:
skills/reverse-engineering/tools.md(Packed Binaries:UPX/自定义壳/PyInstaller) - 仓库:
skills/reverse-engineering/tools-advanced.md(VMProtect/Themida 分析、脱壳框架) - 仓库:
skills/reverse-engineering/patterns.md(自修改代码、Nanomites、多阶段加载器) - 仓库:
skills/reverse-engineering/field-notes.md(Packed 类型笔记) - 仓库:
skills/reverse-engineering/anti-analysis.md(壳常用反调试手段全表) - 实战案例:
skills/field-journal/seed-001_elf-packed-loader.md