关于 unpacking-protected-binaries
unpacking-protected-binaries 是面向恶意软件和二进制分析人员的 Skill,指导在受控环境中动态解包受保护的可执行文件。它侧重从内存转储程序、修复导入表,并说明商业保护器和无法整体转储时的处理方式。
主要功能
- 01识别保护器与打包特征
- 02指导动态解包和 OEP 定位
- 03转储进程并修复导入表
- 04处理 TLS 回调与反调试
- 05提供无法整体转储时的恢复方法
适用场景
- 01分析高熵且代码难以识别的可执行文件
- 02从运行中的样本恢复可分析程序
- 03检查 Themida 或 VMProtect 等保护程序
- 04在无法一次性转储时追踪解密代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trilwu/ secskills --skill unpacking-protected-binaries安装后在 Claude Code 中输入/unpacking-protected-binaries确认已加载。
详细介绍
拆解受保护的二进制文件
静态解包对 UPX 以外的任何程序来说都是个陷阱。可靠的方法是让程序自行解包,然后从内存中提取结果。几乎所有保护器,无论多么复杂,最终都必须在可读页面中生成可执行代码——你要等的就是这一刻。
仅在隔离环境中运行受保护的样本;参见 analyzing-malware。
适用场景
binwalk -E显示文件大部分区域的熵值都很高且均匀- 导入表只有少数几个条目(
LoadLibrary、GetProcAddress) - 节名称缺失、随机,或为
UPX0/.themida/.vmp0 - 反汇编显示一个小型存根和一个大型数据块
- 工具报告文件已加壳,或分析发现其中几乎没有代码
不适用场景
- 更广泛的恶意软件工作流 — 使用
analyzing-malware进行隔离、初步分析和 IOC 输出;再回到这里执行解包步骤 - .NET 程序集 — 使用
analyzing-dotnet-assemblies,其中专门介绍 .NET 加壳程序和托管内存转储 - 未加壳的混淆(导入表可读、熵值正常、控制流混乱)— 使用
analyzing-binaries - 固件映像 —
binwalk -Me提取是另一类工作;参见analyzing-binaries
先识别保护器
通用加壳程序和商业保护器的应对方法差异很大,因此请先花一分钟识别它们。
diec target.exe # Detect It Easy — the best single identifier
rg -a -o 'UPX|MPRESS|Themida|VMProtect|ASPack|Enigma|Obsidium|PECompact' target.exe
binwalk -E target.exe # entropy profile
# Structural tells
readpe target.exe # or: rabin2 -S target.exe
# raw size ≈ 0 with large virtual size → section unpacked at runtime
# section marked writable AND executable → self-modifying
# entry point outside the first section → stub in a later section
# TLS callbacks present → code runs BEFORE the entry point
TLS 回调很重要。 它们会在入口点之前执行,因此,如果调试器设置为在 EP 处中断,保护器的反调试检查早已运行。请将调试器设置为在 TLS 回调处中断,而不是在入口点处中断。
| 识别结果 | 方法 |
|---|---|
| UPX(未修改) | upx -d — 只需几秒 |
| UPX(标头已修改) | 修复魔数,或动态解包 |
| 通用加密器、自定义存根 | 在 OEP 处动态转储 |
| Themida、VMProtect、Enigma | 转储并大幅修复导入;预期虚拟化函数仍会保持虚拟化 |
| .NET 加壳程序 | analyzing-dotnet-assemblies |
upx -d target.exe -o unpacked.exe # try first, costs nothing
动态解包循环
1. Break before the stub runs (TLS callbacks, or the EP if none)
2. Run until the unpacked code exists in memory
3. Find the OEP — the original entry point of the real program
4. Dump the process image
5. Rebuild the import table
6. Fix the PE headers and verify the dump loads
定位 OEP 需要经验判断。可靠的信号包括:
- 先写入再执行内存。 在存根正在写入的节上设置硬件执行断点。首次在该处执行通常就在 OEP 或其附近。在 x64dbg 中,可对目标节设置页保护断点。
- 尾部跳转。 加壳程序存根会以跳转指令结束,跳转目标远在存根自身节之外。单步执行到存根末尾,并留意远距离跳转。
- 编译器入口特征。 真正的入口点看起来像编译器的 CRT 启动代码——
security_init_cookie、对__scrt_common_main的调用,或标准函数序言。当执行落在看起来像正常编译代码而不是混淆存根代码的位置时,就说明你找到了入口。 - 真实程序早期的
GetCommandLine/GetModuleHandle调用。
x64dbg workflow:
Options → Events → break on TLS callbacks and on system breakpoint
Run, then in the Memory Map set "Break on execute" for the target section
When it breaks, confirm the code looks compiler-generated → that is the OEP
转储和导入修复
# Dump from the debugger at OEP: Scylla (built into x64dbg)
# 1. Attach / already broken at OEP
# 2. Scylla → set OEP → IAT Autosearch → Get Imports
# 3. Fix invalid/unresolved entries, then Dump + Fix Dump
# Or dump externally
pe-sieve /pid <pid> /dmode 3 # dumps and repairs; good for automation
# hollows_hunter for scanning a whole system for unpacked/injected modules
大多数转储失败都出在导入修复上。 加壳程序会用运行时解析的存根替换导入表,因此原始转储中的 API 调用会指向加壳程序自身的跳板区域。Scylla 的 IAT 搜索会找到已解析的表;如果返回了未解析的条目,通常意味着:
- 转储时导入尚未完全解析——继续运行程序
- 保护器使用 API 重定向,每次调用都会经过一个用于计算真实地址的混淆存根——这些需要手动解析,或使用基于仿真的存根还原工具
- API 采用首次调用时延迟解析——触发相应功能,然后再转储
分析前请先验证转储:
readpe dumped.exe | head -30 # sane headers, correct EP
rabin2 -i dumped.exe | head -20 # imports resolve to real API names
# The strongest test: does it run, or does a decompiler produce sane output?
商业壳
Themida、VMProtect、Enigma 等工具的功能不止是加壳。请做好以下准备:
- **虚拟化函数。**选定的函数会被转换为自定义 VM 的字节码。转储可以还原其周围的程序,但虚拟化函数仍然只是一个 VM 解释器循环。针对每个加壳工具版本进行去虚拟化,都是研究级别的工作。
- **激进的反调试和反虚拟机机制。**先处理检测问题——使用 ScyllaHide、TitanHide 或虚拟机监控程序级调试器——否则在到达 OEP 前,进程就会退出或自行损坏。
- 多个脱壳阶段,并且会重新加密前面的阶段。
实际决策是:如果只有少数函数经过虚拟化,就转储并分析其他所有内容,然后动态处理这些函数——挂钩它们的输入和输出,而不是阅读其逻辑。这样无需攻破 VM,也能回答“它会做什么”。
无法转储时
有些样本无法在一个位置完整解包:它们会按需解密单个函数,并在使用后重新加密;或者完全从 JIT 风格的缓冲区运行。
- **基于跟踪的恢复。**使用插桩框架记录执行过程,并根据跟踪重建已执行的代码路径。
- 挂钩解密例程,并记录生成的每个明文块——这样可以逐块获取代码,最终得到完整代码。
- 使用 Frida/Pin/DynamoRIO 进行插桩。
应摒弃的自我安慰
- *“
upx -d失败了,所以它不是 UPX。”*修改过的 UPX 标头是最常见的普通规避手段。检查节名称和存根特征。 - *“我要写一个静态脱壳工具。”*如果你会反复遇到某个家族上千次,这么做值得。否则,转储的成本低一个数量级。
- *“转储无法运行,所以它是错的。”*验证你真正需要的内容。许多转储无法运行,但完全可以分析。
- *“导入表坏了,转储毫无用处。”*在执行后续阶段重新转储,或手动解析。问题通常出在时机上。
- *“它用了 VMProtect,所以不可能处理。”*只有虚拟化函数才是。转储其他所有内容,并将这些函数视为具有可观察输入/输出的黑盒。
- *“我要在入口点设置断点。”*TLS 回调早已运行。
- *“运行后什么都没发生。”*反虚拟机或反调试机制已触发。在脱壳前先处理检测问题。
参考资料
analyzing-malware— 遏制、能力模型、IOC;此处流程所适用的工作流analyzing-binaries— 脱壳后的分析和反分析处理analyzing-dotnet-assemblies— .NET 加壳工具和托管代码转储- Detect It Easy、x64dbg + Scylla + ScyllaHide、pe-sieve、hollows_hunter、UPX