完整逆向:自定义运行时解密壳的模拟脱壳实录(睡觉前玩一手)
目标样本:test_packed.exe(Windows x64 GUI)
分析目标:脱壳并还原程序原始逻辑
分析方式:纯用户态静态分析 + Unicorn 引擎模拟执行,全程未运行样本、未依赖调试器
最终产物:test_packed_unpacked.exe(OEP 已还原)、一键脱壳脚本 emu_unpacker.py
0. TL;DR
- 样本为一道 unpack-me 型练习题:壳对原程序代码节做了 RC4 加密,将原程序入口重定向到壳桩,并在运行时通过 ROR13 API hash + PEB 遍历(不使用任何导入表)完成 API 解析。
- 壳内置多层反分析:PEB 标志检查、
IsDebuggerPresent/CheckRemoteDebuggerPresent、窗口扫描(按标题/类名枚举分析工具)、注册表反调试键(NtGlobalFlag/GlobalFlag)、驱动与文件存在性探测、时间反调试等。检测命中后不报错、无窗口、静默退出(退出码 0x5100),这也是"双击无反应"假象的来源。
- 本文使用 Unicorn 引擎伪造最小进程环境(TEB/PEB/LDR + 假
kernel32 导出表),让壳自行完成全部解密,在控制权首次移交原代码时捕获 OEP(0x14000141C,即 mainCRTStartup),随后按运行时镜像重建 PE。
- 脱壳结论:原程序是一个极简 Win32 窗口程序——注册窗口类
winhello,在 WM_CREATE 中无条件创建文本为 hello world! 的 STATIC 子窗口并进入消息循环。全代码不存在任何密码校验逻辑(无输入控件、无字符串比较、无 ASCII 常量比对)。壳层那些看似"密码校验"的检查,全部服务于反分析。
1. 样本信息
| 属性 |
值 |
| 文件名 |
test_packed.exe |
| 大小 |
463,360 字节 (0x71200) |
| MD5 |
59d94d802aba58fb441235b21b65b8ed |
| SHA256 |
3f1e906ebd92e82310939558700dabcfb2c8a5d566e419787fe19f9ff370df86 |
| 架构 |
x86-64 (PE32+),GUI 子系统 |
| ImageBase |
0x140000000(ASLR 下实际基址相同) |
| TimeDateStamp |
0x6ACFDE92(2026-10-14 19:57:06 UTC,即打包时间) |
| 原始入口 |
RVA 0x455E0(位于载荷节 .WJLIEU 内) |
1.1 节表与熵
| 节名 |
VA |
VSize |
Raw |
熵 |
属性 |
备注 |
| .KRrk0s |
0x1000 |
0x5088 |
0x5200 |
7.99 |
R-X |
原程序代码,RC4 加密 |
| .vD6rgs |
0x7000 |
0x287C |
0x2A00 |
4.47 |
RW- |
可读数据 |
| .aWWS4n |
0xA000 |
0x2140 |
0x1000 |
2.11 |
RW- |
字符串/明文数据 |
| .fH1JqT |
0xD000 |
0x0528 |
0x0600 |
3.83 |
R-- |
数据 |
| .rZT03B |
0xE000 |
0x01BE |
0x0200 |
3.99 |
R-- |
RVA 数组表 |
| .WJLIEU |
0xF000 |
0x6787A |
0x67A00 |
7.75 |
RWX |
壳载荷:加密 blob + 壳桩,EP 在此 |
静态特征非常典型:随机 6 字符节名、双高熵可执行节、EP 落在 RWX 载荷节、节内存在 VMPR 字节片段——而导入表却是完整的原程序导入(USER32/GDI32/CRT),字符串(hello world!、winhello 等)亦未加密。这种"导入表完整 + 代码高熵"的组合说明:壳不做导入隐藏,只做代码加密与执行流重定向。
2. 动态行为观察
在分析机上双击运行,现象为进程存在但无任何窗口,数秒后自行退出,退出码 0x5100。对运行中进程做只读内存扫描(ReadProcessMemory,无调试器参与)得到两个关键事实:
.KRrk0s 在内存中依旧保持熵 7.96——壳采用"解密-执行"甚至"按需解密"策略,静止状态下代码不落地;
- 载荷节
.WJLIEU 内除 EP 附近一小段明文壳桩外,其余仍为高熵密文。
结合退出码 0x5100 恰为壳配置表中的常量(见 §4.3),可以判定:静默退出是壳反分析命中的主动行为,而非程序崩溃。这也解释了为什么传统"跑起来再 dump"的路线走不通——必须让壳在受控环境中把反分析全部"骗过",才会走完解密流程。
3. 壳桩逆向(EP 0x455E0)
3.1 主流程
壳桩本体只有约 1.4KB 明文代码,IDA 反编译后主流程清晰:
void start(...) {
save_args_to_globals(...);
if (!initialized) {
sub_4589D(); // 解密器 #2:配置表
sub_4569D(); // 解密器 #1:载荷 blob A
sub_45A9D(); // 解密器 #3:载荷 blob B
if (qword_47340 && qword_47340 < 0x400000)
call(0x1400135E0 + qword_47340); // 二级引擎入口(blob B 反分析)
call(0x1400135E0); // RC4 解密 .KRrk0s
initialized = 1;
}
call(0x140013000); // 壳 runtime 初始化
call(0x140012780); // 移交原程序
}
3.2 三个 stage-1 解密器
三个解密器均为纯算术混淆(MBA 风格垃圾指令填充寄存器运算,不影响语义),全部以 8 字节为单位、自高地址向低地址原地解密:
| 例程 |
目标范围(RVA) |
大小 |
轮数 |
核心变换(q 为 64 位字) |
| sub_4569D |
0xF100 – 0x455E0(blob A) |
222,688 B |
0x6CBC (27836) |
q = ((q − 0x5339EEE4) ⊕ 0xFFFFFFFF9BB60C44) + 0x26EBBE33 |
| sub_4589D |
0x46998 – 0x46AE0(配置表) |
320 B |
0x28 (40) |
q ⊕= 0x7DE94A53 |
| sub_45A9D |
0x47660 – 0x76768(blob B) |
192,968 B |
0x5E39 (24121) |
q = ((q ⊕ 0xFFFFFFFFD62745A5) + 0x782276EC − 0x28E40D3) ⊕ 0x27BE95A2 |
此外 blob B 引擎在执行初期还会解密第四块区域(gap 区,RVA 0x46AE0–0x47660,2,944 B / 368 qword),该区在解密后包含API 指针槽位表与二级引擎跳板代码(0x475A0)。
解密后的 blob A 头部即为合法 x64 代码,第一眼可辨认出 RC4 KSA 的标志性结构(S[i]=i 填充循环 + div 取模的密钥混合循环)。
3.3 配置表(RVA 0x46998,320 字节)
解密后的配置表以 64 位字段组织,关键字段:
| 偏移 |
值 |
含义 |
| +0x10 |
0x140076770 |
blob B 尾部 VA |
| +0x18 |
0x1400015CA |
壳引用的内部地址 |
| +0x20 |
0x5100 |
反分析命中后的静默退出码 |
| +0x28 |
0x140000000 |
ImageBase |
| +0x30 |
0x469A0 |
配置表自引用 |
| +0x38 |
0x7000 |
加密代码节大小 |
| +0x48 |
0x199FA8158B863AFE |
密钥材料 |
| +0x50 |
0x100054DED158 |
RC4 密钥(8 字节小端:58 D1 ED 54 00 10 00 00) |
| +0x58 |
0xF000 |
载荷节 VA |
| +0x60 |
0x379A0 |
blob A 长度 |
| +0x78 |
0x71200 |
原始文件大小(完整性自校验用) |
| +0x80 起 |
24×8 字节 |
高熵密钥/参数材料 |
3.4 RC4 解密原代码节
.KRrk0s(原程序代码,20,616 字节)由 blob A 内的 RC4 例程解密,流程被 VirtualProtect(addr, size, PAGE_EXECUTE_READWRITE, &old) 包裹:
1. key = *(qword*)(0x1400469E8) ; 配置表 +0x50
2. RC4-KSA(S, key, 8)
3. VirtualProtect(0x140001000, 0x5088, RWX, &old)
4. RC4-PRGA ⊕ 全节(逐字节,共 20,616 次写)
5. VirtualProtect 恢复原属性
解密完成后整节熵由 7.99 降至 6.15,即为标准 MSVC 静态链接 x64 代码。
4. API 解析机制:无导入 ROR13
壳桩自身不调用任何导入函数,而是通过 PEB 定位模块、按 ROR13 名称哈希扫描导出表:
FARPROC resolve_by_hash(DWORD hash) {
PEB = __readgsqword(0x60);
Ldr = PEB->Ldr;
for (ent = Ldr->InLoadOrderModuleList.Flink; ...; ent = ent->Flink) {
base = ent->DllBase;
if (fn = scan_export_ror13(base, hash)) // ror32(h,13)+c 逐字符
return fn;
}
return NULL;
}
导出表扫描器(RVA 0xFE94)细节相当完整:校验 DOS/NT/PE32+ 魔数、NT+0x88 取导出目录、遍历 NumberOfNames、命中后还会调用 forwarded export 检查(按节表验证函数 RVA 落点、扫描转发字符串 xxx.yyy 的点号)。
整个壳共请求 54 个唯一 hash,覆盖三类用途:
- 反分析:
IsDebuggerPresent、CheckRemoteDebuggerPresent、OutputDebugStringA、FindWindowA/FindWindowW、GetForegroundWindow、GetWindowTextA/W、GetClassNameA、EnumWindows、PostMessageW/SendMessageW;
- 执行环境:
VirtualProtect/VirtualQuery(改代码节页属性)、LoadLibraryA、GetModuleHandleA、CreateFileA/W + ReadFile + GetFileSize + CloseHandle(文件探测)、GetSystemDirectoryA、GetModuleFileNameA;
- 完整体的运行时支撑:
ExitProcess、GetCurrentThread、GetTickCount/QueryPerformanceCounter(时间反调试)、MultiByteToWideChar 等。
5. 反分析体系
blob B(192KB)是壳的反分析引擎,解密后可见大量环境探测字符串与逻辑:
| 类别 |
手段 |
| 进程级 |
IsDebuggerPresent、CheckRemoteDebuggerPresent、PEB NtGlobalFlag/GlobalFlag |
| 窗口级 |
EnumWindows + GetWindowTextA/W + GetClassNameA 按标题/类名扫描分析工具;FindWindowA/FindWindowW、GetForegroundWindow |
| 注册表 |
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\... 下 AppInit_DLLs、NtGlobalFlag、GlobalFlag(IFEO 反调试) |
| 文件/驱动 |
C:\Windows\System32\drivers\ac.sys 存在性探测、\\.\PhysicalDrive0、%SystemRoot%\system32\ntdll.dll 校验 |
| 时间 |
GetTickCount、QueryPerformanceCounter 差值检测单步 |
| 行为 |
引擎内含 SetThreadContext 等上下文操作;错误统一走 err=0x%08X 格式化后静默退出 |
所有检查不弹窗、不提示:命中即以配置表约定的退出码(0x5100)静默结束进程。这意味着传统"先运行再附加"的动态分析路线会全军覆没,也让静态分析者误以为存在某种"密码校验失败"路径——实际上壳内不存在任何面向用户的密码输入与比较逻辑。
6. Unicorn 模拟脱壳
6.1 思路
既然壳的所有解密都是纯计算(三个解密器 + RC4 无任何系统调用),而 API 解析依赖 PEB 遍历,最干净的方案是用 Unicorn 伪造一个最小进程环境,让壳自己把一切解出来:
- 按原 ImageBase(0x140000000)映射全部节;
- 伪造
TEB → PEB → Ldr → 单模块链(kernel32.dll),GS 段基址指向 TEB;
- 手工构造一个结构正确的假 kernel32:PE32+ 头、节表、导出目录(约 290 个常见导出),导出函数体为
mov rax, 槽位; jmp rax,槽位处放一条 ret;
- 样本自身 IAT 槽位同样指向 API 槽位区;
- 在 API 槽位地址挂
UC_HOOK_CODE 实现 API 语义(VirtualProtect 恒成功、HeapCreate 返回非空句柄、IsDebuggerPresent 恒 0、窗口枚举类恒"未找到"等);
- 挂
UC_HOOK_MEM_WRITE 监控加密节的写入量与熵变化;
- OEP 捕获:壳 runtime 全部位于载荷节内,一旦指令指针首次落入解密后的
.KRrk0s,即为控制权移交点——本样本首次移交地址为 0x140002CD8(__security_init_cookie,由壳 runtime 调用),据此停止模拟并 dump。
6.2 实现要点与踩坑
- 假 kernel32 的 PE 结构必须经得起壳的解析器:壳的 forwarded-export 检查会遍历节表、用
NT+0x88 取导出目录、严格校验 SizeOfOptionalHeader(NT+0x14)。任何字段错位(例如把 SizeOfOptionalHeader 写到 NT+0x0C)都会让全部 hash 解析静默失败,表现为"解析器调用正常但结果全为 NULL"。
- 导出目录字段顺序:
NumberOfFunctions 与 AddressOfFunctions 错一位会让扫描器把函数地址表当序号表用,症状同样是"hash 匹配成功但返回 NULL"。
- null-call 处理:未解析的 API 槽为 NULL,壳中存在
call rax(rax=0) 路径。在 Unicorn 里手动改 RIP 续跑会触发 UC_ERR_MAP,正确做法是直接映射零页并放置 xor eax,eax; ret,让空调用自然"返回 0"。
- 堆初始化语义:CRT 的
HeapCreate 若返回 0,mainCRTStartup 会走 exit(0xff) 分支——模拟日志中令人困惑的 ExitProcess(0xff) 由此而来,属环境问题而非样本逻辑。
- 数据节保真:重建时仅替换代码类节,数据节保留磁盘原版,IAT 维持未解析状态交给真实 loader 处理。
6.3 一键化
上述流程已封装为 emu_unpacker.py,自动完成:加密节识别(可执行 + 初始熵 > 7 且不含 EP)→ 环境伪造 → 模拟执行 → 解密监控(本样本 20,616 字节写入,熵 7.99→6.15)→ OEP 捕获 → 节回填 → 假指针槽回滚 → EP 重定向 → 校验和修正。
python emu_unpacker.py test_packed.exe -o test_packed_unpacked.exe [--oep 0x14000141C]
7. OEP 与文件重建
- 真 OEP:
0x14000141C(mainCRTStartup)。壳将 EP 改写为自身桩(0x455E0),原入口由壳 runtime 在完成解密与运行时初始化后移交。
- 首次进入解密节的地址为
0x140002CD8(__security_init_cookie,壳 runtime 在移交前调用),脚本同时支持 --oep 手动指定最终入口。
- 重建策略:
.KRrk0s 回填为解密后数据(原样,不做任何"清洗"——按窗口清洗伪指针会破坏指令流,是常见的重建事故);
.WJLIEU 回填为运行时镜像,并仅对壳在模拟期间写入的 API 假指针槽做定点回零(通过写钩子精确记录,避免误伤代码);
- 数据节(.vD6rgs/.aWWS4n/.fH1JqT/.rZT03B)保留磁盘原版,IAT 交由真实 loader 重建;
- 入口点写回 OEP,重算 CheckSum。
重建后验证:入口处为标准 mainCRTStartup(GetStartupInfoW → PE 头自检 → CRT 初始化链),.KRrk0s 内 WndProc(RVA 0x1000)、WinMain(RVA 0x1130)反汇编与动态行为完全吻合。
8. 解包结论:程序的"真实面目"
脱壳后的程序是一个教科书级的极简 Win32 窗口程序(MSVC 静态 CRT):
// WinMain (0x140001130)
RegisterClassExA(&(WNDCLASSEXA){
.lpfnWndProc = WndProc, .lpszClassName = "winhello", ... });
CreateWindowExA(0, "winhello", "winhello", WS_OVERLAPPEDWINDOW, 居中, 0x140x0x78, ...);
ShowWindow / UpdateWindow;
while (GetMessageA(...)) { TranslateMessage; DispatchMessageA; }
// WndProc (0x140001000)
case WM_CREATE: // 0x1400010C9
CreateWindowExA(0, "STATIC", "hello world!",
WS_CHILD|WS_VISIBLE|SS_CENTER,
0, 0, 0, 0, hwnd, (HMENU)100, hInst, 0);
case WM_SIZE: // 0x14000106B
MoveWindow(GetDlgItem(hwnd, 100), ...); // 居中重排
case WM_DESTROY:
PostQuitMessage(0);
case WM_CTLCOLORSTATIC:
SetBkColor(...); return GetSysColorBrush(...);
对解密后全部 20,616 字节代码做了系统排查:无输入控件(无 EDIT)、无 GetCommandLineA 参与的业务逻辑(仅 CRT 标准调用)、无任何字符串/常量比较分支(ASCII 立即数扫描零命中)。窗口与其中的 hello world! 是无条件创建的。
因此,本题的"密码"并不存在于程序逻辑中——壳层的全部"校验"(哈希解析、环境探测、窗口扫描、注册表检查、时间检测)共同构成一道反分析屏障:在含分析工具的环境里,程序连窗口都来不及创建就静默退出(0x5100),制造出"有密码校验"的错觉;而在干净环境中,它只是坦然地弹出一个写着 hello world! 的窗口。解开这道题的钥匙从来不是某个字符串,而是把壳本身拆开。
附录 A:ROR13 请求清单(54 项)
0x73E2D87E ExitProcess 0xA36DC676 IsDebuggerPresent
0x43AF7D80 CheckRemoteDebuggerPresent 0x470D22BC OutputDebugStringA
0x75DA1966 GetLastError 0xF791FB23 GetTickCount
0xEA7AF15B QueryPerformanceCounter 0x249466D5 (未命名,自定义 hash)
0xE8CDCFE4 GetCurrentThread 0x2D3FCD70 (未命名)
0xEC0E4E8E LoadLibraryA 0x6228050D (未命名)
0x673F23AF (未命名) 0x3B1E50C8 FindWindowA
0x6215F501 GetForegroundWindow 0xA1CCB963 GetWindowTextA
0xB3C52468 GetClassNameA 0x0021E71A EnumWindows
0x51E20CE0 (未命名) 0x84454957 (未命名)
0xB9A87739 (未命名) 0x7AC67C03 (未命名)
0x8FDE2C7E TranslateMessage 0x690A1717 (未命名)
0x94305BE0 DestroyWindow 0xC95D4F83 ShowWindow
0xC2BFD83F UpdateWindow 0x7B66EC0D PostMessageW
0x4BE0469D PostQuitMessage 0xEB6CC40A SendMessageW
0xCBA6C0E5 (未命名) 0x16F8BA2A (未命名)
0xD53A404E (未命名) 0xA1CCB979 GetWindowTextW
0xEF4AC4E4 MultiByteToWideChar 0xD3324904 GetModuleHandleA
0xB10FD839 (未命名) 0xE4E1CAD6 (未命名)
0xE3D6909C (未命名) 0x00D83695 (未命名)
0xFEEDF510 (未命名) 0xE935E393 (未命名)
0xDCD44C5F (未命名) 0x159B3EA0 (未命名)
0xABF6E97A (未命名) 0x45B06D76 GetModuleFileNameA
0x7C0017A5 CreateFileA 0x7C0017BB CreateFileW
0x10FA6516 ReadFile 0xDF7D9BAD GetFileSize
0x0FFD97FB CloseHandle 0xB8E579C1 GetSystemDirectoryA
0x7946C61B VirtualProtect 0xA3C8C8AA VirtualQuery
("未命名"项为壳自定义的补充 hash,不在常见导出名集中;对应槽位在正常流程中未触发调用,不影响脱壳。)
附录 B:关键地址速查
| 地址 (RVA) |
角色 |
| 0x455E0 |
壳桩 EP(明文) |
| 0x4569D / 0x4589D / 0x45A9D |
stage-1 解密器 #1/#2/#3 |
| 0xF100 |
RC4 KSA(blob A 头部) |
| 0xFE94 |
ROR13 导出表扫描器 |
| 0x1014C |
PEB 遍历 + hash 解析入口 |
| 0x101DC |
完整性失败处理(擦除 + 退出) |
| 0x134C0–0x135E0 |
RC4 解密调度(含 VirtualProtect 包裹) |
| 0x475A0 / 0x48DA0 |
二级引擎入口 / blob B 反分析引擎 |
| 0x46998 |
配置表(320B) |
| 0x141C |
真 OEP:mainCRTStartup |
| 0x1000 / 0x1130 |
WndProc / WinMain |
| 0x10C9 |
WM_CREATE:创建 "hello world!" STATIC |
附录 C:产物清单
| 文件 |
说明 |
test_packed_unpacked.exe |
脱壳重建文件(MD5 556a332a4142322161d4e8e2f2e53e98,EP=0x141C) |
emu_unpacker.py |
一键模拟脱壳脚本(Unicorn + Capstone + Pefile) |
分析环境:Windows x64;工具链:Unicorn Engine(CPU 模拟)、Capstone(反汇编)、Pefile(PE 解析)。