win脱壳6 -- windows 脱壳思路 技术总结

win脱壳6 -- windows 脱壳思路 技术总结

目录

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、ASPackVMProtect、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 → 获取导入表成功 → 独立运行成功 ✅

两条暗线贯穿全程:

  1. 时机线:壳没跑完(Step 1 之前)→ 内存里是密文,一切无从谈起;壳跑完停在 OEP → 内存状态最干净。
  2. 因果线:崩溃(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 判定标准(怎么确认"真找到了")

  1. 跳转目标之后是函数序言push ebp; mov ebp,esp(32 位)/sub rsp, xx(64 位)
  2. 代码呈线性结构(正常函数调用),不再有解密循环
  3. 最终验证:dump 修复后,DIE 能看到原始编译器特征

4.4 定位 IAT 的方法论

在 OEP 下硬件执行断点 → F9 让原始代码跑起来 → 代码执行到第一处 call [IAT] → 拿到的是 stub 地址 → 跳转报错 → 报错的那一行(如 004714B1)就是 IAT 条目 → 内存跟值 → 跨段(ds)→ 看到 GetVersion → 100% 确认

报错行 ≠ 崩溃位置:报错行是call [IAT]指令本身;崩溃发生在跳转之后的无效地址。两者要分清。

4.5 样本关键数据(脱壳全程的"坐标")

模块基址0x00400000
OEP0x0047148Bpush ebp函数序言)
OEP RVA0x7148B(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 3ESP 定律自动化:把程序带到"壳跑完、代码解密、IAT 重建"的状态
③ IAT 遍历循环 + 判 0 + EIP 指向 + STI 到 RET + 回填Junk+RET 规律:批量抓真地址

关键设计决策

  1. 范围取宽(0x75000 起,实际有效 0x75080 起)——故意多遍历,靠"判 0 跳过"兜底,防止漏掉条目。
  2. 为什么读 ESP 要在 RET 之前:脚本停在 RET 指令上(未执行),此时 [ESP] 是 RET将要弹出的值;执行完 RET 再读,ESP 已移动,拿不到了。
  3. STI 循环而不是直接跑:junk 指令无规律,只有 RET 是确定的收尾标志,所以一条条单步到 RET。
  4. 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 成功

七、常见坑速查表

#对策
1Dump 后运行崩溃不是工具坏了,是IAT 未修复——回到 Step 3~6
2报错行 ≠ 崩溃位置报错行是 call [IAT] 指令,崩溃发生在跳转后的无效地址
364 位没有 pushad/pushfdESP 定律失效;改用对 .text 下内存访问断点;条目步长 8;DWORD→QWORD
4硬件断点只有 4 个用 DR0~DR3,省着用,用完即清
5RET 不只有 0xC3还有RET n(0xC2 xx xx);脚本判断要扩展
6stub 里有 callSTI 会误入子函数,误判子函数 RET;改用 STO 或加"RET 后 [ESP] 是模块内地址"双重验证
7脚本死循环stub 内可能一直无 RET;加步数上限保护,先跑 3~5 条验证
8脚本修完 ≠ 完成脚本只还原"条目里的值",Scylla Fix Dump 负责重建导入表结构,缺一不可
9ASLR/重定位dump 时运行时基址 ≠ 默认基址时需修重定位表,否则换机器/加载地址就崩
10Get Imports 出现乱码/未知函数IAT 范围没扫全或识别失败,手动修正范围
11"找到 OEP" ≠ 脱壳完成OEP + Dump + Fix IAT 三者缺一不可

八、免杀项目视角

脱壳能力在免杀项目中的三个用途

  1. 自测:给自己加壳后手动脱一遍——验证壳是否达到"静态不可识别"的目的(用攻击者的手验证防御者的眼)。
  2. 对抗:了解杀软内存脱壳引擎的工作方式(找 OEP、识别 stub 规律),反推自己壳的设计。
  3. 分析:研究真实恶意样本的壳和混淆手法。

反向应用

脱壳方的抓手加壳方的反制
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 自动化接口