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

SkillDev tools

反混淆:花指令、控制流平坦化、字符串加密。触发词:反混淆、花指令、控制流平坦化、字符串解密、obfuscation

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the 反混淆(花指令 / 控制流平坦化 / 字符串加密) skill

What this skill tells your AI

The instructions your AI receives, as published by dslsdzc/rev-skills in .claude/skills/re-deobfuscate/SKILL.md and read by ahel’s review.

何时使用 / 何时不用

  • 用:反编译产物出现花指令(反汇编碎片、恒等跳转)、控制流平坦化(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、查表、逐字节变换)。
    • 动态读(需沙箱,[[re-analyze/platform-tips]] 最高原则):[[re-gdb]] / [[re-x64dbg]] 断在解密函数返回处,x/s 读结果。
    • 仿真:小循环用 python3 复刻(按逆向出的算法与 key):
      # 例:单字节 XOR 解密
      key, out = 0x7f, bytearray()
      for b in open('data.bin','rb').read():
          out.append(b ^ key)
      print(out[:64])
      
    • 对比动态与仿真结果,一致后进入批量。
  4. 批量脚本化(解密所有字符串)

    • idapython:遍历引用解密函数的 xref,调用后把结果写入注释 / 输出文件:
      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]],见 [[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_hexraysmblock.npred()/pred()/tail/typemop_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 X9B 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 次不同运行);后端能力缺失报告为未知而非负证据

Signals

GitHub stars
57
Forks
8
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
re-deobfuscate
Source
github.com/dslsdzc/rev-skills