mcpField
D
deobf-indirect

deobf-indirect

github.com/p4nda0s安全14097 次安装23 周前更新
代码仓库

关于 deobf-indirect

deobf-indirect 是用于分析和修补通过 CSEL 或 CSET 模式混淆的 ARM64 间接分支的逆向技能。其脚本采用符号执行和广度优先遍历,并对含有有效指令的跳转表情形单独处理。

主要功能

  1. 01识别 CSEL 与 CSET 分支模式
  2. 02通过符号执行推导分支目标
  3. 03使用 BFS 遍历代码块
  4. 04修补二进制文件中的间接分支
  5. 05保留 CSET 情形中的有效指令
  6. 06支持 Mach-O 和 ELF 变体

适用场景

  1. 01分析混淆的 ARM64 控制流
  2. 02还原间接跳转的分支目标
  3. 03修补基于 CSEL 的分支
  4. 04保留设置代码并处理 CSET 跳转表

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/p4nda0s/bin-deobf-skills --skill deobf-indirect

安装后在 Claude Code 中输入/deobf-indirect确认已加载。

附带文件script/deinbr-v3-csel.pyscript/deinbr-v3-cset.py

详细介绍

反混淆间接分支

第 1 部分:CSEL + BR 间接分支模式

模式识别

混淆器会将条件分支转换为间接跳转,其中 CSEL 和 BR 指令成对出现。CSEL 根据条件选择两个目标地址之一,随后执行一些无用指令,最后由 BR 跳转到选定的地址。

典型指令序列:

CMP             W10, W11
CSEL            W10, W13, W12, LS   ; select W13 or W12 based on LS condition
......                               ; junk code in between (address calculations, etc.)
ADD             X8, X8, X14
BR              X8                   ; indirect jump to the computed target

主要特征:

  • CSEL 从两个寄存器值中进行选择,它们分别代表两个分支目标
  • CSEL 与 BR 之间的代码用于计算地址或属于无用代码
  • BR 执行最终的间接跳转
分析方法

符号执行:遇到 CSEL 指令时,强制选择不同的分支,从而获得两个不同的 BR 目标地址。

步骤:

  1. 从同一个基本块入口开始运行两次符号执行
  2. 第一次运行:强制 CSEL 选择第一个寄存器(条件为真分支)
  3. 第二次运行:强制 CSEL 选择第二个寄存器(条件为假分支)
  4. 每次运行都会到达 BR,并得到不同的目标地址(A 和 B)
遍历策略

从函数入口块开始进行 BFS 遍历:

  1. 将函数入口地址加入工作队列
  2. 取出一个地址,分别使用两种 CSEL 选择方式运行符号执行,直到遇到 BR
  3. 记录补丁信息(CSEL 地址、条件、两个目标地址)
  4. 将两个目标地址都加入工作队列
  5. 使用已访问集合避免重复访问
  6. 将以 RET 结尾的块标记为返回块,不再从这些块继续遍历
补丁策略

CSEL 与 BR 之间的代码是无用代码——在 CSEL 位置直接补丁为条件分支。

例如,CSEL W11, W8, W9, CC:W8 是 CC 条件为真的分支目标(A),W9 是 CC 条件为假的分支目标(B)。

补丁如下:

BCC  A    ; if condition met, jump to A
B    B    ; otherwise jump to B

两条指令共 8 字节,会覆盖 CSEL(4 字节)和下一条无用指令(4 字节)。

第 2 部分:CSET + BR 间接分支模式(跳转表变体)

模式识别

与第 1 部分的 CSEL 变体不同,CSET 变体使用 0/1 索引查询跳转表来计算目标地址。CSET 与 BR 之间的代码并非全是无用代码,其中包含后续基本块所依赖的有效指令。

典型指令序列:

CMP             X27, X8
CSET            W8, EQ              ; W8 = 0 or 1 (index)
STR             W8, [SP, #offset]   ; store index (junk)
LDR             X9, [SP, #tbl_off]  ; load jump table pointer (junk)
LDR             X8, [X9, W8, UXTW#3] ; table[index] (junk)
ADRP            X9, #page           ; load encrypted constant (junk)
LDR             W9, [X9, #off]      ;   (junk)
MOV             W10, #imm           ; XOR key (junk)
MOVK            W10, #imm, LSL#16   ;   (junk)
EOR             W9, W9, W10         ; decrypt offset (junk)
NEG             W9, W9              ; negate (junk)
ADD             X8, X8, W9, SXTW    ; final target address (junk, boundary)
; --- useful code below ---
ADRP            X25, #0x100004000   ; register setup for successor blocks
ADD             X25, X25, #0x250
MOV             W28, #0xF065        ; constant init
MOVK            W28, #0x611A, LSL#16
LDR             X23, [SP, #0x50]    ; load state for successor
BR              X8                   ; indirect jump (junk)
无用代码识别

ADD Xn, Xn, Wm, SXTW 是无用代码与有效代码的分界点。从 CSET 到 ADD(含 ADD)的所有内容都是无用代码:

指令用途分类
CSET Wd, cond设置 0/1 索引无用代码(由补丁替换)
STR Wd, [SP, #off]存储索引值无用代码(仅供跳转表使用)
LDR Xn, [base, #off]加载跳转表指针无用代码
LDR Xm, [Xn, Wd, UXTW#3]查表 table[index]无用代码
ADRP + LDR Wn加载加密常量无用代码
MOV + MOVK WmXOR 解密密钥无用代码
EOR Wn, Wn, Wm解密偏移量无用代码
NEG Wn, Wn取负无用代码
ADD Xm, Xm, Wn, SXTW计算最终地址无用代码(分界点)
后续的 MOV/LDR/STR/ADRP+ADD初始化寄存器和栈状态有效代码
BR Xm间接跳转无用代码(由补丁替换)
分析方法

与 CSEL 变体相同:符号执行分别强制 CSET 取 1 和 0,运行到 BR,以获得两个目标地址。

有一点不同:单个基本块可能包含多个 CSEL/CSET 指令(例如,先是数据选择 CSEL,随后是控制分支的 CSET)。脚本会强制选择每个遇到的 CSEL/CSET;记录的 csel_addr 是最后一个(即控制 BR 目标的那个)。

补丁策略

无法直接在 CSET 所在位置打补丁(这样会跳过有用代码)。正确做法:

  1. 定位 ADD Xn, Xn, Wm, SXTW(边界)
  2. 提取 ADD+4 与 BR 之间的有用代码字节
  3. 将有用代码上移至 CSET 所在位置
  4. 紧接其后追加 Bcond A; B B
Before:  [CSET][junk...][ADD][useful code][BR]
After:   [useful code][Bcond A][B B][... dead code ...]

前提条件:有用代码必须与位置无关(SP 相对寻址、立即数赋值或同一页内的 ADRP)。在同一个 4KB 页面内移动 ADRP 无需调整立即数。

参考实现

针对不同混淆子模式的两个脚本变体:

script/deinbr-v3-csel.py — CSEL 变体(ELF)

适用于 CSEL 直接在两个目标地址之间进行选择,且 CSEL 与 BR 之间的代码纯属地址计算垃圾代码的二进制文件。在 CSEL 所在位置打补丁,覆盖 8 个字节(Bcond + B),替换 CSEL 和下一条垃圾指令。

script/deinbr-v3-cset.py — CSET 变体(Mach-O / ELF)

适用于 CSET 设置 0/1 索引,且 CSET 与 BR 之间的代码包含有用的副作用指令(寄存器设置、供后续代码块使用的栈存储),并与地址计算垃圾代码交错的二进制文件。

补丁策略:找到 ADD Xn, Xn, Wm, SXTW(最后一个地址计算步骤),将有用代码(ADD 与 BR 之间的代码)上移至 CSET 所在位置,然后追加 Bcond + B。这样可以保留后续代码块所依赖的寄存器/内存状态。移动的指令必须与位置无关(SP 相对寻址、立即数或同一页内的 ADRP)。

两个脚本共享相同的核心工作流:

  1. analyze_br(proj, func_start) — BFS 遍历,收集所有补丁点
  2. run_until_br(proj, state, csel_selector) — 从给定状态执行至 BR,并强制选择 CSEL/CSET 分支
  3. do_patch(binary_path, save_path, proj, patch_list) — 使用 keystone 汇编补丁并写入二进制文件

关键 angr 选项:

  • CALLLESS — 忽略函数调用,防止分析结果偏离
  • LAZY_SOLVES — 延迟约束求解,以提升性能
  • ZERO_FILL_UNCONSTRAINED_MEMORY — 用零填充未约束内存
  • ZERO_FILL_UNCONSTRAINED_REGISTERS — 用零填充未约束寄存器
脚本执行规则

所有非简单的 Python 代码(包括分析脚本和最终补丁脚本)都必须写入文件,并通过 python script.py 执行。切勿在 bash 中内联使用 python -c "..."。这样可以避免 shell 引号转义问题,也让脚本更易于调试和复用。

常见错误与调试
BR 目标是无效地址
block: 41fe20, next_1: <SimState @ 0x2908f8c3>, next_2: <SimState @ 0x2908f8c3>
SimEngineError: No bytes in memory for block starting at 0x2908f8c3.

原因:用户提供的地址不是函数入口,而是内部基本块。基于栈的跳转表基址和偏移常量尚未初始化(被 ZERO_FILL 填充为零),导致 BR 计算出无效目标。

解决方案:确认该地址是函数起始位置。使用符号表或 IDA/Ghidra 查找函数入口:

for sym in proj.loader.main_object.symbols:
    if sym.rebased_addr <= target_addr < sym.rebased_addr + sym.size:
        print(sym.name, hex(sym.rebased_addr))
两个分支目标相同
block: XXXXX, next_1: <SimState @ 0xABCD>, next_2: <SimState @ 0xABCD>

原因同上——从非入口点开始执行,意味着 CSEL 选中的两个寄存器值相同(均为零或相同的未初始化值),因此地址计算后得到相同的 BR 目标。

解决方案:使用正确的函数入口地址。

遇到非 CSEL 的条件选择指令
block XXXXX: expected 1 successor, got 0

或者脚本到达 BR 时未检测到 CSEL,导致全局变量中没有 csel_addr。

原因:基本块使用的是 CSET,而不是 CSEL。CSET 是 CSEL 的一种特殊形式,等价于 CSEL Rd, WZR, WZR, invert(cond),会选择 1 或 0 作为索引。

按处理 CSEL 的相同方式处理——识别条件并强制选择两个分支。脚本必须同时处理 csel 和 cset:

if insn.mnemonic == 'cset':
    # cset Wd, cond  is equivalent to  csel Wd, #1, #0, cond (condition met=1, not met=0)
    dst, cond = parse_cset(insn)
    val = 1 if csel_selector == 1 else 0
    setattr(state.regs, dst, val)
    ...

评论

支持 Markdown