目录
ESP 定律 + IAT 去混淆脚本修复 -- 脱壳技术分析
一、先对号入座:这套方法适用于什么壳
二、核心原理:三大支柱
支柱 1:壳的生命周期是一段"固定模式"的代码序列
支柱 2:ESP 定律
支柱 3:IAT 混淆规律(Junk + RET)
三、完整流程:六步闭环(脱壳全程地图)
四、关键技术细节清单
4.1 断点选择
4.2 步进选择
4.3 OEP 判定标准(怎么确认"真找到了")
4.4 定位 IAT 的方法论
4.5 样本关键数据(脱壳全程的"坐标")
4.6 Dump 时机与工具
五、脚本:核心资产(完整版 + 设计拆解)
六、核心公式与知识总结
七、常见坑速查表
八、免杀项目视角
脱壳能力在免杀项目中的三个用途
反向应用
ESP 定律 + IAT 去混淆脚本修复 -- 脱壳技术分析
一、先对号入座:这套方法适用于什么壳
| 判断维度 | 适用 | 不适用(虚拟化壳) |
|---|---|---|
| 壳入口特征 | pushad/pushfd保存现场 | 无 pushad,入口即虚拟化分发 |
| 原始代码处理 | 内存中解压/解密,可还原 | 指令翻译成虚拟机字节码,无法还原 |
| IAT 处理 | Junk stub + RET 重定向 | 深度虚拟化,无显式 IAT |
| 代表 | UPX、ASPack | VMProtect、Themida |
识别方法:
- DIE 查壳名 + 观察入口指令。入口以 pushad/pushfd 开头 → 本方法可直接上。
- 看到 0xCC 反调试、花指令堆砌、入口即巨型分发器 → 换思路。
二、核心原理:三大支柱
支柱 1:壳的生命周期是一段"固定模式"的代码序列
pushad ; ① 保存 8 个通用寄存器(现场) pushfd ; ② 保存标志位寄存器 call 壳核心函数 ; ③ 解密/解压、重定位、重建 IAT(壳的"正事") ... ; 中间过程不用分析 popfd / popad ; ④ 恢复现场 ← 壳"寿终正寝"的信号 jmp OEP ; ⑤ 跳回原始入口 ← 要抓的就是这一跳推论:脱壳不需要"读懂"壳代码,只需要识别壳的收尾点。壳的执行路径是固定模式(压栈 → 干活 → 弹栈 → 跳转),找到"恢复现场"就找到了 OEP。
支柱 2:ESP 定律
- pushad 把 8 个寄存器的值压入栈中,此后只有 popad 会再次访问这些值
- 对当前 ESP 下硬件"访问"断点→ 壳收尾执行 popad 时必然踩中 → 断点命中 = 壳运行完毕、代码已解密
- 断点命中后向下看:跳转指令的下一条(或跳转目标)就是 OEP
pushad 执行后 → ESP 指向压栈区域 → 下硬件访问断点 ↓ popad 读取栈 → 断点命中 → 壳跑完了 ↓ 向下找 jmp / jmp eax → F7 跟进 → OEP支柱 3:IAT 混淆规律(Junk + RET)
call [IAT条目] ← 代码里的调用 ↓ IAT条目 → 壳stub地址 ← 不再是 API 地址(混淆的真相) ↓ stub 内:一堆 junk 垃圾指令(push/pop/add esp,无意义) ↓ RET ← stub 必然以 RET 收尾 ↓ RET 弹出 [ESP] 上的值 → 跳到真实 API关键认知:混淆是"绕路",不是"消失"。程序要正常工作,就必须真正调用 GetVersion、GetProcAddress 这些 API——真实地址必然在某处出现(本样本规律:RET 执行前 ESP 上就是真地址)。去混淆 = 找到它出现的位置并抓回来。
三、完整流程:六步闭环(脱壳全程地图)
Step 0 壳识别 DIE 查壳名 → 确认传统壳 → 确认入口 pushad 特征 ↓ Step 1 找 OEP (5.2) 停EP → F8过pushad → 对ESP下硬件访问断点 → F9 → 命中在popad → 找跳转 → F7跟进 → OEP ↓ Step 2 Dump验证 (5.3) OEP处Scylla Dump → 运行崩溃 → 判定:IAT未修复(必然翻车点,不是工具坏了) ↓ Step 3 定位IAT (5.4) OEP下硬件执行断点 → 运行 → 第一处call[IAT]报错 → 内存跟值 → ds段看到GetVersion → 确认IAT位置 ↓ Step 4 分析混淆 (5.5) IAT区域全是call/jmp → 确认Junk+RET模式 → 手动验证1~2条规律成立 ↓ Step 5 脚本修复 (5.6/5.7) .scr脚本批量遍历IAT → 逐条回填真实API地址 ↓ Step 6 重新Dump (5.7) Scylla → 获取导入表成功 → 独立运行成功 ✅两条暗线贯穿全程:
- 时机线:壳没跑完(Step 1 之前)→ 内存里是密文,一切无从谈起;壳跑完停在 OEP → 内存状态最干净。
- 因果线:崩溃(Step 2)暴露 IAT 问题(Step 3 定位)→ 分析混淆规律(Step 4)→ 设计并执行修复(Step 5)→ 修复后重新 dump 才能得到健康文件(Step 6)。一次 dump 不够,修复后必须重新 dump。
四、关键技术细节清单
4.1 断点选择
| 断点类型 | 用途 | 特点 / 坑 |
|---|---|---|
| 软件断点(F2,0xCC) | 常规断点 | 改写代码字节,强壳完整性自校验会检测到 → 脱壳慎用 |
| 硬件执行断点 | OEP 观察、定位 IAT | 用 CPU 调试寄存器 DR0~DR3,不修改代码;最多 4 个 |
| 硬件访问断点 | ESP 定律 | 等 popad读栈时命中(断点类型选 Access,不是 Execute) |
| 内存断点 | 64 位替代方案 | 对 .text 下访问断点,等壳解压后第一次读原始代码时命中 |
4.2 步进选择
| 操作 | 用在哪 |
|---|---|
| F7 步入 | call壳核心函数(必须进,壳的工作在里面);jmp eax间接跳转(必须进才能看到真实地址);stub 内部 |
| F8 步过 | pushad、普通指令 |
| F4 运行到光标 | 跳过解密大循环(别傻傻单步) |
4.3 OEP 判定标准(怎么确认"真找到了")
- 跳转目标之后是函数序言:
push ebp; mov ebp,esp(32 位)/sub rsp, xx(64 位) - 代码呈线性结构(正常函数调用),不再有解密循环
- 最终验证:dump 修复后,DIE 能看到原始编译器特征
4.4 定位 IAT 的方法论
在 OEP 下硬件执行断点 → F9 让原始代码跑起来 → 代码执行到第一处 call [IAT] → 拿到的是 stub 地址 → 跳转报错 → 报错的那一行(如 004714B1)就是 IAT 条目 → 内存跟值 → 跨段(ds)→ 看到 GetVersion → 100% 确认报错行 ≠ 崩溃位置:报错行是call [IAT]指令本身;崩溃发生在跳转之后的无效地址。两者要分清。
4.5 样本关键数据(脱壳全程的"坐标")
| 项 | 值 |
|---|---|
| 模块基址 | 0x00400000 |
| OEP | 0x0047148B(push ebp函数序言) |
| OEP RVA | 0x7148B(OEP − 基址,Scylla 里填这个) |
| IAT 首地址 | 0x00475000(存放 stub 地址 0x005E2264) |
| IAT 实际有效范围 | 0x00475080 ~ 0x00475120 |
| IAT 结束地址 | 0x00475120(不包含) |
| 条目步长 | 4 字节(32 位)→ 共约 72 个条目 |
| stub 收尾标志 | RET(0xC3),RET 前 [ESP] = 真实 API 地址 |
4.6 Dump 时机与工具
- 时机:必须停在 OEP,未执行原始代码(此时:代码已解密 ✅、IAT 已重建 ✅、无运行时副作用 ✅)
- 工具链:x64dbg + Scylla(首选,Dump + 修复一站式)、ImportREC(老牌)、OllyDump、LordPE、Process Hacker(纯取证,不用于脱壳)
五、脚本:核心资产(完整版 + 设计拆解)
bc ; 清所有普通断点 bpmc ; 清所有内存断点 bphc ; 清所有硬件断点 StepInto 2 ; 单步步入2次(执行过 pushad,ESP 指向压栈区) bph esp+4, r, 4 ; esp+4 下读型硬件断点,4字节 g ; 运行 → popad 读栈命中 → 壳运行完毕 StepInto 3 ; 再步入3次,越过 popad,进入跳向 OEP 区域 $dwIatBegin = 0x00475000 ; IAT 遍历起点 $dwIatEnd = 0x00475120 ; IAT 遍历终点 IATLOOP: mov $dwIatItemAdr, dword [$dwIatBegin] ; 读条目值(stub地址) cmp $dwIatItemAdr, 0 ; 判空条目 jz IATNEXT ; 为0跳过(防误改空白区) mov EIP, $dwIatItemAdr ; 模拟 call [IAT],进入 stub STEPLOOP: sti ; 单步步入(F7) mov $byteCode, byte:[EIP] ; 读当前指令首字节 cmp $byteCode, 0xC3 ; 判断 RET jnz STEPLOOP ; 不是RET继续走junk mov $FindFuncAdr, dword [ESP] ; ★RET前读ESP = 真实API地址 mov [$dwIatBegin], $FindFuncAdr ; ★回填IAT条目 ← 修复完成 IATNEXT: add $dwIatBegin, 4 ; 下一个条目(步长4) cmp $dwIatBegin, $dwIatEnd jne IATLOOP ret脚本的三段式结构(对应三大支柱):
| 段落 | 做什么 | 对应原理 |
|---|---|---|
| ① 清场 | BC / BPMC / BPHC | 避免残留断点干扰 |
| ② ESP 定位 | StepInto 2 → bph esp+4 → g → StepInto 3 | ESP 定律自动化:把程序带到"壳跑完、代码解密、IAT 重建"的状态 |
| ③ IAT 遍历 | 循环 + 判 0 + EIP 指向 + STI 到 RET + 回填 | Junk+RET 规律:批量抓真地址 |
关键设计决策
- 范围取宽(0x75000 起,实际有效 0x75080 起)——故意多遍历,靠"判 0 跳过"兜底,防止漏掉条目。
- 为什么读 ESP 要在 RET 之前:脚本停在 RET 指令上(未执行),此时 [ESP] 是 RET将要弹出的值;执行完 RET 再读,ESP 已移动,拿不到了。
- STI 循环而不是直接跑:junk 指令无规律,只有 RET 是确定的收尾标志,所以一条条单步到 RET。
- EIP = [条目]模拟
call [IAT]:让 stub 自己跑起来吐出真地址,无需分析 junk 内容。
六、核心公式与知识总结
OEP + 修复的 IAT = 可运行 PE
脱壳的本质 = 找到正确的 OEP(壳"吐"出原始代码的落点)+ 修复被破坏的 IAT(把 stub 地址还原为真实 API 地址)。两者齐备,文件才真正脱离壳独立运行。
全部知识点浓缩(对应各节总结):
| 主题 | 一句话 |
|---|---|
| 壳边界 | OEP 是原始入口,pushad/popad 标志壳边界 |
| 找 OEP | 断点监控恢复现场(ESP 定律) |
| IAT 与免杀 | IAT 加密避检测,修复填充真地址 |
| 混淆形式 | 混淆用 Junk + RET(stub 以 RET 收尾,RET 弹出 ESP 真地址) |
| 修复手段 | 脚本自动化遍历 IAT(循环 + 判 0 + 回填) |
| 单步策略 | STI 单步,RET 判断结束 |
| 最终结果 | 执行修复 IAT,Dump 成功 |
七、常见坑速查表
| # | 坑 | 对策 |
|---|---|---|
| 1 | Dump 后运行崩溃 | 不是工具坏了,是IAT 未修复——回到 Step 3~6 |
| 2 | 报错行 ≠ 崩溃位置 | 报错行是 call [IAT] 指令,崩溃发生在跳转后的无效地址 |
| 3 | 64 位没有 pushad/pushfd | ESP 定律失效;改用对 .text 下内存访问断点;条目步长 8;DWORD→QWORD |
| 4 | 硬件断点只有 4 个 | 用 DR0~DR3,省着用,用完即清 |
| 5 | RET 不只有 0xC3 | 还有RET n(0xC2 xx xx);脚本判断要扩展 |
| 6 | stub 里有 call | STI 会误入子函数,误判子函数 RET;改用 STO 或加"RET 后 [ESP] 是模块内地址"双重验证 |
| 7 | 脚本死循环 | stub 内可能一直无 RET;加步数上限保护,先跑 3~5 条验证 |
| 8 | 脚本修完 ≠ 完成 | 脚本只还原"条目里的值",Scylla Fix Dump 负责重建导入表结构,缺一不可 |
| 9 | ASLR/重定位 | dump 时运行时基址 ≠ 默认基址时需修重定位表,否则换机器/加载地址就崩 |
| 10 | Get Imports 出现乱码/未知函数 | IAT 范围没扫全或识别失败,手动修正范围 |
| 11 | "找到 OEP" ≠ 脱壳完成 | OEP + Dump + Fix IAT 三者缺一不可 |
八、免杀项目视角
脱壳能力在免杀项目中的三个用途
- 自测:给自己加壳后手动脱一遍——验证壳是否达到"静态不可识别"的目的(用攻击者的手验证防御者的眼)。
- 对抗:了解杀软内存脱壳引擎的工作方式(找 OEP、识别 stub 规律),反推自己壳的设计。
- 分析:研究真实恶意样本的壳和混淆手法。
反向应用
| 脱壳方的抓手 | 加壳方的反制 |
|---|---|
| pushad → popad 固定模式 | 打乱保存现场方式(x64 天然没有 pushad) |
| stub 统一"junk + RET"规律 | stub 多样化、多层套娃、花指令 |
| RET 前 ESP 暴露真地址 | 不在 RET 暴露(用 call 直接转、混淆 ESP) |
| 硬件断点不被检测 | 用完整性自校验检测 0xCC 软件断点、反调试检测 DR 寄存器 |
| 单步可控 | 反单步:时间检测、异常机制打断单步流 |
清醒认知:
- 现代 EDR 重心已从静态扫描转向行为检测(沙箱、内存扫描、API 钩子监控)。
- 加壳只能骗过静态这一层——payload 在内存解压后,行为照样被看见。
- 壳是免杀整体方案的一环,不是全部。
九、学习路径延伸
样本(pushad + Junk-RET 混淆)✅ 已掌握 ↓ UPX 手动脱壳(单步法 / ESP 定律各脱一遍)——结构标准,验证基本功 ↓ ASPack / ASProtect —— 引入反调试,学 ScyllaHide 绕过 ↓ 64 位样本 —— 无 pushad,重学断点策略 ↓ 强壳(VMProtect / Themida)—— 知道"脱壳"在这里已升级为"反虚拟化"即可"写壳脱壳框架"的泛化方向
- 把脚本参数化:IAT 范围、步长、RET 判定、stub 模式(判 0 / 判 RET / 步数上限)都做成可配置
- 壳变体越多,识别规则越要灵活——"规律识别"(找收尾模式)比"硬编码地址"更接近框架
- 可研究 x64dbgpy(Python API)写更复杂的自动化,或 Scylla 自动化接口