mcpField
D
deobf-indirect

deobf-indirect

github.com/p4nda0sセキュリティ14097 回インストール23 週間前に更新
リポジトリ

deobf-indirect について

deobf-indirectは、CSELまたはCSETパターンで難読化されたARM64間接分岐を分析・パッチするリバースエンジニアリング用スキルです。シンボリック実行と幅優先探索を使い、有用な命令を含むジャンプテーブルも個別に処理します。

主な機能

  1. 01CSELとCSETの分岐パターンを識別
  2. 02シンボリック実行で分岐先を導出
  3. 03BFSによるブロック探索
  4. 04バイナリ内の間接分岐をパッチ
  5. 05CSETの場合に有用な命令を保持
  6. 06Mach-OとELFの変種に対応

活用シーン

  1. 01難読化されたARM64制御フローの分析
  2. 02間接ジャンプの分岐先を特定
  3. 03CSELベースの分岐をパッチ
  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 は条件に基づいて 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 ターゲットアドレスを取得します。

手順:

  1. 同じ基本ブロックのエントリから、シンボリック実行を 2 回開始します
  2. 1 回目の実行:CSEL に最初のレジスタを選択させます(条件が真の分岐)
  3. 2 回目の実行:CSEL に 2 つ目のレジスタを選択させます(条件が偽の分岐)
  4. それぞれの実行は BR に到達し、異なるターゲットアドレス(A と B)を取得します
トラバーサル戦略

関数エントリブロックを起点として、BFS トラバーサルを実行します。

  1. 関数のエントリアドレスを作業キューに追加します
  2. アドレスをキューから取り出し、CSEL の両方の選択肢で BR までシンボリック実行します
  3. パッチ情報(CSEL アドレス、条件、2 つのターゲットアドレス)を記録します
  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

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, cond0/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 まで実行して 2 つのターゲットアドレスを取得します。

異なる点は、1 つの基本ブロックに複数の 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)。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)。

両方のスクリプトで、次のコアワークフローを共有しています。

  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 "..." をインラインで使用しないでください。これにより、シェルの引用符やエスケープに関する問題を避けられ、スクリプトのデバッグや再利用も容易になります。

よくあるエラーとデバッグ
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)
    ...

コメント