deobf-indirect について
deobf-indirectは、CSELまたはCSETパターンで難読化されたARM64間接分岐を分析・パッチするリバースエンジニアリング用スキルです。シンボリック実行と幅優先探索を使い、有用な命令を含むジャンプテーブルも個別に処理します。
主な機能
- 01CSELとCSETの分岐パターンを識別
- 02シンボリック実行で分岐先を導出
- 03BFSによるブロック探索
- 04バイナリ内の間接分岐をパッチ
- 05CSETの場合に有用な命令を保持
- 06Mach-OとELFの変種に対応
活用シーン
- 01難読化されたARM64制御フローの分析
- 02間接ジャンプの分岐先を特定
- 03CSELベースの分岐をパッチ
- 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 は条件に基づいて 2 つのターゲットアドレスのいずれかを選択し、その後にジャンク命令が続き、最後に 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は 2 つのレジスタ値のいずれかを選択します。これらの値は 2 つの分岐先を表しますCSELとBRの間のコードは、アドレス計算またはジャンクコードですBRが最終的な間接ジャンプを実行します
分析方法
シンボリック実行:CSEL 命令に遭遇したら、異なる分岐選択を強制して、2 つの異なる BR ターゲットアドレスを取得します。
手順:
- 同じ基本ブロックのエントリから、シンボリック実行を 2 回開始します
- 1 回目の実行:
CSELに最初のレジスタを選択させます(条件が真の分岐) - 2 回目の実行:
CSELに 2 つ目のレジスタを選択させます(条件が偽の分岐) - それぞれの実行は
BRに到達し、異なるターゲットアドレス(A と B)を取得します
トラバーサル戦略
関数エントリブロックを起点として、BFS トラバーサルを実行します。
- 関数のエントリアドレスを作業キューに追加します
- アドレスをキューから取り出し、
CSELの両方の選択肢でBRまでシンボリック実行します - パッチ情報(CSEL アドレス、条件、2 つのターゲットアドレス)を記録します
- 両方のターゲットアドレスを作業キューに追加します
- 再訪を防ぐため、訪問済みセットを使用します
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
2 命令で 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 Wm | XOR 復号キー | ジャンク |
EOR Wn, Wn, Wm | オフセットを復号 | ジャンク |
NEG Wn, Wn | 符号を反転 | ジャンク |
ADD Xm, Xm, Wn, SXTW | 最終アドレスを計算 | ジャンク(境界) |
| 以降の MOV/LDR/STR/ADRP+ADD | レジスタおよびスタック状態の初期化 | 有用 |
BR Xm | 間接ジャンプ | ジャンク(パッチで置き換え) |
分析方法
CSEL 版と同じく、シンボリック実行で CSET にそれぞれ 1 と 0 を取らせ、BR まで実行して 2 つのターゲットアドレスを取得します。
異なる点は、1 つの基本ブロックに複数の CSEL/CSET 命令が含まれる場合があることです(たとえば、データ選択用の CSEL の後に、分岐を制御する CSET が続く場合)。スクリプトは、遭遇したすべての CSEL/CSET で選択を強制します。記録される csel_addr は最後のもの(BR のターゲットを制御するもの)です。
パッチ適用の方針
CSET の位置に直接パッチを適用することはできません(有用なコードがスキップされるためです)。正しい方法は次のとおりです。
ADD Xn, Xn, Wm, SXTW(境界)を特定します- ADD+4 から BR までの有用なコードバイト列を抽出します
- 有用なコードを CSET の位置まで移動します
- その直後に
Bcond A; B Bを追加します
Before: [CSET][junk...][ADD][useful code][BR]
After: [useful code][Bcond A][B B][... dead code ...]
前提条件:有用なコードは位置独立である必要があります(SP 相対アドレッシング、即値代入、または同じページ内の ADRP)。ADRP を同じ 4KB ページ内で移動する場合、即値の調整は不要です。
リファレンス実装
異なる難読化サブパターンに対応する 2 種類のスクリプトがあります。
script/deinbr-v3-csel.py — CSEL 版(ELF)
CSEL が 2 つのターゲットアドレスから直接選択し、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)。
両方のスクリプトで、次のコアワークフローを共有しています。
analyze_br(proj, func_start)— BFS 探索を行い、すべてのパッチ箇所を収集しますrun_until_br(proj, state, csel_selector)— 指定した状態から BR まで実行し、CSEL/CSET の選択を強制します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 "..." をインラインで使用しないでください。これにより、シェルの引用符やエスケープに関する問題を避けられ、スクリプトのデバッグや再利用も容易になります。
よくあるエラーとデバッグ
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 によって選択される 2 つのレジスターに同じ値(どちらもゼロ、または同一の未初期化値)が入るため、アドレス計算後の BR のターゲットも同じになります。
解決策:正しい関数のエントリーポイントを使用してください。
CSEL 以外の条件付き選択命令が検出される
block XXXXX: expected 1 successor, got 0
または、スクリプトが CSEL を検出せずに BR に到達し、csel_addr が globals に存在しない場合です。
原因:基本ブロックで CSEL の代わりに CSET が使用されています。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)
...