re-deobfuscate

Helps developers reverse-engineer hidden or scrambled code to understand what it does.

Installation
Run `npx skills add "https://github.com/dslsdzc/rev-skills" --skill "re-deobfuscate"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

What this skill does
What it does: - Removes obfuscation techniques that make code hard to read, including junk instructions mixed in with real code, control flow that has been scrambled into switch statements, and encrypted strings - Restores readable code by cleaning up garbage instructions and fixing the original program flow - Decrypts hidden text strings that are stored encrypted in the program - Automates the process of finding and decrypting all hidden strings at once When to use it: - You are analyzing code that has been intentionally scrambled to hide what it does - You see strings that are encrypted or hidden instead of stored as plain text - The program flow looks like a giant switch statement instead of normal if statements and loops - You need to understand the real logic of a program after it has been unpacked from a protective shell
SKILL.mdShow the author's original SKILL.md (not in English)
---
name: re-deobfuscate
description: 反混淆:花指令、控制流平坦化、字符串加密。触发词:反混淆、花指令、控制流平坦化、字符串解密、obfuscation
---

# 反混淆(花指令 / 控制流平坦化 / 字符串加密)

## 何时使用 / 何时不用

- 用:反编译产物出现花指令(反汇编碎片、恒等跳转)、控制流平坦化(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);命令行跑脚本:`idat64 -A -S"script.py" sample.exe`
- 验证: IDA 内 `File > Script Command` 能执行 Python

### rizin 脚本(批量 patch / 查询)

- Linux: `apt install rizin` / `dnf install rizin` / `pacman -S rizin`;macOS: `brew install rizin`;Windows/WSL: WSL 内 Linux 包
- 验证: `rizin -v`;配合 `pip install r2pipe` 用 Python 驱动

### 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 r2pipe`
- 验证: `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、查表、逐字节变换)。
   - 动态读(需沙箱,[[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 / r2pipe:`rizin -A sample -c 'axt @ sym.decrypt'` 列引用,脚本循环解密,导出 `strings_decrypted.txt` 供 [[re-ioc]] / 人工分析。
   - 产出:全量解密字符串表(地址 → 明文),存档进分析记录。

5. **还原前后对比验证**:
   - `sha256sum` 记录修改前后;`objdump -d` 对比花指令区域字节差异。
   - 重新反汇编确认无 "undefined"/ 无未定义跳转目标(每步做完即查,别攒到最后)。
   - 重新反编译目标函数,确认控制流与调用关系合理;沙箱内运行验证行为一致([[re-sandbox]],见 [[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 次不同运行);后端能力缺失报告为未知而非负证据

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us