Back to skills
SKILL.md
Re Deobfuscate
ASecurity反混淆:花指令、控制流平坦化、字符串加密。触发词:反混淆、花指令、控制流平坦化、字符串解密、obfuscation
- 112 stars
- 0 votes
- 0 copies
- 3 views
- Added September 19, 2026
Works with
Security analysis
92/100- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add dslsdzc/rev-skills --skill re-deobfuscate --agent claude-codeAre you the author of Re Deobfuscate?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dslsdzc-re-deobfuscate)---
name: re-deobfuscate
description: 反混淆:花指令、控制流平坦化、字符串加密。触发词:反混淆、花指令、控制流平坦化、字符串解密、obfuscation
capabilities: [deobfuscation]
---
# 反混淆(花指令 / 控制流平坦化 / 字符串加密)
## 何时使用 / 何时不用
- 用:反编译产物出现花指令(反汇编碎片、恒等跳转)、控制流平坦化(if/while 全变 switch 分发)、字符串加密(静态只见密文数组);需要还原算法 / 授权逻辑
- 用:CTF 反混淆题目;脱壳后仍有代码混淆的样本([[re-anti-analysis]] 工作流第 5 步)
- 不用:干净代码(直接 [[re-binary-core]] 分析)
- 不用:只需绕过保护、不关心算法(动态 patch / hook 更省,见 [[re-gdb]] / [[re-frida]])
- 不用:壳层混淆(那是壳的解压逻辑——先脱壳 [[re-unpack-simple]] / [[re-unpack-advanced]])
- 注意:反混淆是迭代过程——还原一层验证一层,先备份原文件
## 工具准备
### 反编译产物(还原的工作台)
- [[re-ghidra]]:`apt install ghidra` / `brew install --cask ghidra`(官方 release zip 也可),验证 `analyzeHeadless -help`;或 [[re-ida]] / [[re-radare2]] 的反编译视图。先产出反编译代码,再针对混淆点处理
### idapython(批量脚本化)
- IDA 内置(8.x/9.x 自带 Python 3);命令行无头跑脚本的可执行文件名与用法见 [[re-ida]]
- 验证: IDA 内 `File > Script Command` 能执行 Python
### rizin 脚本(批量 patch / 查询)
- 安装矩阵见 [[re-radare2]],安装后以 `rizin -v` 验证
- Python 驱动用 Rizin 侧绑定 `rzpipe`(`pip install rzpipe`,rizinorg/rz-pipe)
### D-810(IDA 插件,可选)
- 原仓 GitLab `eshard/d810`(GitHub 镜像 `zhkl0228/d810`,原仓 README 已 fork 注明)release,复制到 IDA `plugins/` 目录(要求 IDA 7.5+ / Python 3.7+,来源:官方 README)
- 功能:控制流平坦化自动还原(Deobfuscate 菜单)
- 验证: IDA 菜单出现 D-810 项
### python3(仿真 / 批量解密)
- `apt install python3`(多数系统自带);按需 `pip install pefile rzpipe`
- 验证: `python3 --version`
## 操作步骤
按顺序执行,每步记录结果(证据路径 + sha256,见 [[re-triage]])。**修改前先备份原文件**(见坑 1)。
1. **花指令清除(patch NOP / 跳转修复)**:
- 识别特征:恒等跳转(`jz`/`jnz` 下一句即目标)、`call`+`pop` 取址、插入的垃圾指令(`xor eax,eax` 后无意义)、反汇编器误入垃圾字节形成的碎片。
- 清除:把垃圾指令 patch 成 NOP,修复被扰乱的跳转目标——
- rizin: `wx 90 @ 0x401000`(单字节 NOP);批量用脚本按特征扫描填充
- Ghidra: 选中区域右键 Patch Instruction;IDA: `Edit > Patch Program > Assemble`
- 修复函数边界:Ghidra 选中范围按 `C` 强制标记代码 / 右键 Create Function;反汇编器漏分析的段手动定义。
- 每步 patch 后重新反汇编确认无 "undefined" 指令(见坑 1)。
2. **控制流平坦化识别与还原(D-810 / 手动)**:
- 识别特征:大量基本块收敛到单一 dispatcher(`switch` 分发循环)、状态变量(dispatcher 的索引)在每个块尾部被改写、原 if/while 分支块被拆成小块。
- 自动:IDA + D-810 → `Deobfuscate` 菜单 → `Flattened code`,选中平坦化函数一键还原(失败回落手动)。
- 手动:先定位 dispatcher 的 switch 变量 → 跟踪其写入点(每块尾部)→ 把各 case 目标按状态变量连接回原始控制流;用 idapython / rizin 脚本导出 dispatcher 的目标表辅助连线。
- **状态变量找错是最大风险**(见坑 2)——先用 D-810 自动,手动时先确认变量确实参与 dispatcher 索引。
3. **字符串解密循环定位与仿真**:
- 静态定位:数据节找密文数组(高熵 / 无明文)→ xref 找引用它的函数(或引用 `memcpy`/`strcpy` 前指针)→ 分析解密循环(XOR 单字节 / 多字节 key、查表、逐字节变换)。
- 动态读(需沙箱,[[re-analyze/platform-tips]] 最高原则):[[re-gdb]] / [[re-x64dbg]] 断在解密函数返回处,`x/s` 读结果。
- 仿真:小循环用 python3 复刻(按逆向出的算法与 key):
```python
# 例:单字节 XOR 解密
key, out = 0x7f, bytearray()
for b in open('data.bin','rb').read():
out.append(b ^ key)
print(out[:64])
```
- 对比动态与仿真结果,一致后进入批量。
4. **批量脚本化(解密所有字符串)**:
- idapython:遍历引用解密函数的 xref,调用后把结果写入注释 / 输出文件:
```python
import idaapi, idc
# 例:枚举引用 sym_decrypt 的调用点,取参数地址后 idc.get_bytes 解密并打印
```
- rizin / rzpipe:`rizin -A sample -c 'axt @ sym.decrypt'` 列引用,脚本循环解密,导出 `strings_decrypted.txt` 供 [[re-ioc]] / 人工分析。
- 产出:全量解密字符串表(地址 → 明文),存档进分析记录。
5. **还原前后对比验证**:
- `sha256sum` 记录修改前后;`objdump -d` 对比花指令区域字节差异。
- 重新反汇编确认无 "undefined"/ 无未定义跳转目标(每步做完即查,别攒到最后)。
- 重新反编译目标函数,确认控制流与调用关系合理;沙箱内运行验证行为一致([[re-sandbox]],见 [[re-analyze/platform-tips]] 最高原则)。
- 字符串解密结果用运行验证交叉确认(动态解密值 == 脚本仿真值)。
## 跨域联合
- [[re-anti-analysis]]:工作流第 5 步(反混淆)固定调用本技能(脱壳后仍有混淆)
- [[re-analyze]] 的 triage「CTF 赛题」路径:re-ctf → re-deobfuscate(反混淆还原)
- [[re-ctf]]:CTF 反混淆题目(花指令 / 平坦化 / 字符串解密)
- [[re-binary-core]]:深度静态逻辑分析(还原后的干净产物继续 [[re-ghidra]] / [[re-ida]] 深挖)
- 配套:[[re-gdb]] / [[re-x64dbg]](动态读解密结果)、[[re-sandbox]](动态验证沙箱)、[[re-ioc]](解密字符串作为特征来源)、[[re-memdump]](运行时密文在内存时才需要)
## 常见坑与陷阱
- **删错指令 → 控制流损坏(先备份)**:现象——patch 后函数无法反编译 / 跳转进垃圾字节 / 运行崩溃;原因——花指令与真实指令混编,误删了有效指令;对策——**patch 前先备份原文件**(sha256 存档),小步 patch + 每步重新反汇编确认无 undefined 指令,损坏时从备份重来
- **平坦化状态变量找错 → 错乱**:现象——还原后 if/else 分支全部乱序、逻辑荒谬;原因——把普通数据变量当成了 dispatcher 状态变量;对策——先确认变量确实作为 switch 索引(值域 = 分支数、每块尾部被改写),优先用 D-810 自动还原,手动时跟踪变量生命周期验证
- **加密字符串需等运行时(动态解密)**:现象——静态分析找不到明文,甚至找不到解密函数;原因——字符串在运行时才解密(解密循环可能也在混淆代码里);对策——调试器断解密函数动态读取(沙箱内),或先还原解密循环再仿真;冷数据(运行时才解密的常量)配合 [[re-memdump]] 从内存取
- **混淆不止一层**:现象——还原完平坦化又发现字符串还是密文,或花指令里套花指令;原因——多层混淆叠加是常见配置;对策——逐层还原、每层跑一遍步骤 5 验证,别试图一步到位
- **Opaque predicate(恒真/恒假分支)**:现象——还原后出现大量"看似条件、实际恒定"的分支,patch 后逻辑怪异,符号执行([[re-angr]])在其处卡死 / 路径爆炸;原因——混淆器(VMProtect 系 / Tigress 等)插入永真/永假条件(如 `x*x-x>=0`、奇偶恒等)扩展路径干扰分析;对策——识别后直接确定分支目标(恒真取真支、恒假取假支)批量化简,先消 opaque predicate 再做平坦化还原与符号执行
- **反 CFF 七步框架与工具选型(IDA 微码)**:现象——手还原 CFF 凭经验乱试;原因——缺系统化流程;对策——按七步:a. 找 dispatcher(BLT_2WAY 块且前驱最多)b. 找有效块 c. 找状态变量(dispatcher tail 是 jcond 且与常量比较)d. 哪个块对状态变量设值 e. 哪个块入口状态变量等于什么 f. 建立 Block→Block 映射 g. 改控制流;工具对比:IDA 内置值域分析(有短板,非标准 CFF 无法完成 e 步)< D810(MicroCodeInterpreter 模拟执行分发器逻辑,较强,框架值得借鉴)< angr 符号执行(无需显式找状态变量,一把梭但重型);IDA 微码 API:`ida_hexrays` 的 `mblock.npred()/pred()/tail/type`、`mop_r/mop_S/mop_d` 递归取变量
- **VM 混淆 vs 平坦化的识别**:现象——把虚拟机混淆当平坦化处理(找状态变量/常量衔接)处处对不上;原因——两种混淆机制不同:平坦化每个基本块末尾有常量衔接另一块,VM 混淆是取字节码+跳转表分发(操作数编码进指令);对策——识别要点:函数头部申请**异常大的栈空间**(VM 的 context/内存,未初始化直接传指针进 VM 函数)、数据段有字节码区与跳转表、入口先取 4 字节拆位域(操作码/寄存器索引);VM 识别后走 VM 还原流程
- **VM 还原方法论(字节码 + 跳转表 + 多级 opcode)**:现象——VM 函数反编译看不懂(大量平行基本块 + dispatch);原因——handler 由字节码索引跳转表分发;对策——①定位字节码区(数据段,统计长度/指令宽度求指令数,如 0x2F4/4=189 条)②统计 opcode 频率(高频优先分析)③跳转表 = 基址 + opcode×4,逐 handler 分析④注意**多级 opcode**(op1=47 时再看 op2 决定二级跳转表;opcode 位域宽度要数清,如 6 位 op + 5 位寄存器 = 4 字节指令)⑤梳理真实寄存器角色(虚拟 PC 指针、控制变量、虚拟寄存器数组基址)⑥控制变量机制(跳转/调用/退出通过设置控制变量实现,handler 尾部公共块 + 取指头部都检查它)
- **VM 只调外部函数时无需完全还原**:现象——为还原加密算法死磕 VM 全部 handler;原因——VM 里可能只是调用外部加密函数(AES/随机数等),字节级处理在 VM 外;对策——先确认 VM handler 是否只做"取参/调用外部函数/存结果"——是则只还原参数传递与调用序列即可调试出算法内容,不必逐字节还原 VM;但注意同系列算法(tt 系)的签名算法可能把加密做进 VM,此时仍需完整还原
- **CFF 误判 → DSVM(领域特定虚拟机)识别**:现象——OLLVM 检测器报告大量 CFF 函数(如 49 个),按平坦化还原却处处对不上;原因——实为借鉴 VMP 架构(跳转表分发 + 字节码解释)但指令集完全领域化的自定义 VM(DSVM,如 UE4 遍历引擎、加固 so);对策——追查异常信号:函数内大量 syscall(OLLVM 只改控制流不引入系统调用)、单函数多调度器(标准 OLLVM 每函数一个分发器);VMP vs DSVM 四维对比:处理器语义(通用 vs 领域特定 98%)、循环结构(单层平面 vs 递归下降解析)、字节码格式(紧凑二进制 vs 文本式魔数+单字节 op)、虚拟栈(有 vs 无);DSVM 是语义级混淆——理解"它在做什么"(对象遍历/协议解析)比"它怎么做的"(操作码分发)更重要
- **PAC 序言漏检函数边界**:现象——函数计数/边界分析结果偏少,单个巨型函数里实际藏着多个函数;原因——ARM64 PAC(Pointer Authentication Code)保护函数用非传统序言,只按 `stp x29,x30` 检测会漏;对策——扩展三种序言模式检测,函数数可大增(238→291);PAC/BTI 着陆点反而是函数边界与间接跳转目标的精确标记,利用而非绕过
- **跳转表条目验证(防字符串区误判)**:现象——跳转表解析出一堆指向数据/字符串区的"目标";原因——rodata 同时含跳转表(16 位偏移数组)与 C++ demangler 名称表,条目可能指向字符串区;对策——验证每个条目的目标地址在 .text 段内,排除指向字符串区的条目;**静态提取跳转表 + Unicorn 动态验证调度流**(合成字节码输入如 `"gs1a"` 追踪执行路径)动静结合是 VM 还原的必要组合
- **间接跳转/调用(BR/BLR X8)去除**:现象——伪代码见 `__asm { BR X8 }` 或 `v278 = v277(...)`(无跳转符号),IDA 控制流图断裂(分支未识别进函数体);原因——OLLVM 间接混淆:目标地址经复杂逻辑运算后存寄存器再跳转,静态无法确定目标;对策——**动态执行取寄存器值**(断点停在 BR/BLR 处读 X8),人工计算目标地址效率太低;拿到目标后 keypatch 统一 patch 成直接跳转(`BR X9` → `B 0x153AF0`);**CSEL 条件分支拆解**:`X8 = (X0>0) ? X8 : X9` 三目表达式逐段计算两个目标,patch 成条件跳转;多条件链(EQ 系列)同理逐个拆
- **CSEL 与 BR 之间的真实指令**:现象——patch 掉 CSEL 后控制流错乱/真实指令缺失;原因——CSEL 指令与 BR 指令之间可能存在真实指令,若在 CSEL 与其下一条之间 patch,B 指令后的指令全不执行;对策——patch 位置必须选在 **CSEL 指令与 BR 指令之间**(保留中间真实指令),不是 CSEL 下一条
- **AI 时代新对抗面:对抗文本诱导 AI 拒绝分析**:现象——代码扔给 AI(ClaudeCode 等)几秒被扒干净,传统混淆效力下降;原因——LLM 逆向能力太强;对策(防御视角/研究):构造对抗性文本后缀(GCG 类算法:LLM 是离散词表分类器,连续梯度不可直接使用——把梯度当启发式信号,算每个位置替换 Token 对损失函数的贡献,Top-K(如 256)候选 + 前向验证贪心挑选;黑盒场景用近似模型/GA 演化/蒸馏代理)与混淆代码结合,诱导 AI 输出拒绝分析;识别 AI 正在分析的信号并做反制(如识别到推理链后注入混淆引导);注意合规边界(合法保护知识产权代码,不得用于隐藏恶意代码)
- **自定义 DSL VM(纯 JS 虚拟机)**:现象——大 JS 文件被当 WASM/常规混淆;原因——自定义解释器循环 + 自定义 opcode;对策——DSL VM 五步:case 提取 → opcode 分类 → 常量表分析 → 函数追踪 → 导出提取;先确认是纯 JS 解释器再决定工具链
- **VM 导出函数隐藏**:现象——VM 文件中找不到导出函数名;原因——导出名被指令编码隐藏;对策——从模块注册中心提取真实导出(不通过 VM 文件表面暴露)
- **流程就绪 ≠ 会话建立**:现象——接口返回成功但流程没起来;原因——成功只是会话确认;对策——以特定状态码/阶段标志判断流程真正就绪,别拿会话确认当启动
(来源:reverse-skill field-journal,MIT)
- **Z3 求解状态机转移撞调用边界**:现象——符号执行还原平坦化时,跨调用继续求解得到错误转移;求解器超时/未知时仍照改,控制流错乱;原因——调用会作废寄存器与内存绑定,默认是硬屏障;「不可达路径」与「未解出」被混为一谈;对策——对每个 case 块做有界符号执行、求解块末状态变量值,调用默认屏障,仅显式证明不逃逸的私有状态才允许跨调用保留;失败区分「证明不可达」(可丢弃)与「超时/未知」(fail-closed 不动边);求解器设超时上限,翻译结果与解缓存复用
- **自动化改写缺覆盖率门槛**:现象——自动还原平坦化后函数体大面积不可达、反编译错乱;原因——解出的转移边太少就动手改 CFG,且改写不留可回滚痕迹;对策——应用前整体安全检查(源/目标块合法、无自环、终止指令形态与后继数匹配),任一条不满足整体放弃;解出转移边数未达 case 块数下限(如三分之一且不少于 3 条)即拒绝改写;字节级修改走可逆补丁保留原始字节(绝不改输入文件),复杂改写计划在事务边界内应用——先快照、逐步应用、任一步被拒整体回滚
- **UNSAT 当恒真/恒假直改**:现象——求解器返回 UNSAT 就断定谓词恒真/恒假直接改边,或被模拟观察到的相反分支反向推翻结论;原因——全称断言的三档证据语义没分清:SAT 模型只说明存在某路径,UNSAT 只能作佐证,能否决全称的只有「具体执行的相反分支观察」(反例);对策——反例须满足资格才有否决权(覆盖全部依赖代码与只读数据、无 trace 截断、无摘要代答、无权限违反、未逃逸镜像);静态证不出恒真时保留间接操作、只记录候选目标;「无法判定」(依赖输入/超时/后端缺能力)一律不动,缺失信息是未知而非负证据
- **勘察与变换混在一条通道**:现象——一上来就全库变换、每函数重建缓存微码、拿全库扫描当普通查询,成本失控;原因——勘察通道与变换通道未分离,调用前没按成本/副作用分级;对策——勘察只走只读检测通道(无缓存微码构建、不装变换组件)全库扫候选,只对候选做变换;成本分级——纯查询零成本零副作用、检测类每函数一次无缓存构建但不落缓存、变换类才改字节与 AST(可逆补丁)、全库扫描既贵又写库;显式准入——只有被请求的函数才走变换管线,准入与变换分两步;探索类操作同步有界、单函数,不轮询、不后台线程、不重复全库扫描
- **检测标志当成果报告**:现象——报告「检测到 N 处混淆」而实际一个字节未改;重复变换同一函数不生效,或变换后伪代码没变化就以为失败;原因——检测标志只是结构候选,没有匹配的可守卫改写就不产生任何变化;缓存与跟踪未管理,重复变换被幂等拦截却不知情;对策——报告实际处理计数与实际应用的修改,不用检测标志充当成果;迭代循环:检测→(可选)探索取证据→变换→无缓存重反编译→再检测/比对伪代码确认收敛;要重跑必须先清函数级跟踪与证据;失败按返回值处理(数值返 0/-1、字符串返空串),不靠异常捕获;变换前确认自动分析已完成、反编译器可用
- **模拟证据越界当全称证据**:现象——模拟引擎观察到相反分支就当反例用,外部调用被摘要代答后仍支撑全称结论,函数已变还拿旧观察说话;原因——模拟证据有边界(单函数有界、运行数有限),摘要代答的语义是宿主模型部分代偿,证据带谱系但消费前未校验;对策——模拟只做单函数有界(镜像快照只作内存上下文),外部调用用摘要代答但标记为探索性、不用于全称证明,未建模目标报告为「环境边界」而非解码故障;每条观察带谱系(库、函数字节哈希、镜像哈希、函数代数、种子),消费前把函数块与 trace 区间逐字节比对当前库,陈旧即弃;动态观察有最小运行数消费门槛(如 2 次不同运行);后端能力缺失报告为未知而非负证据
Attribution
Comments
Loading comments…