code-obfuscation-deobfuscation について
code-obfuscation-deobfuscationは、難読化されたネイティブバイナリを扱うリバースエンジニア向けの分析ガイドです。難読化のパターンを見分け、静的・動的解析、シンボリック実行、エミュレーションを使って構造と挙動を調べます。
主な機能
- 01難読化パターンの分類
- 02不透明述語とジャンクコードの分析
- 03自己書き換えコードの調査
- 04制御フロー平坦化の復元
- 05VM保護と文字列の分析
- 06逆アセンブル妨害の分析
活用シーン
- 01制御フロー平坦化されたバイナリの復元
- 02実行時に復号されるコードの調査
- 03VM保護されたバイナリの分析
- 04隠された文字列やインポートの特定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill code-obfuscation-deobfuscationインストール後、Claude Code で/code-obfuscation-deobfuscationを入力して読み込まれたか確認します。
詳細
SKILL:コード難読化と難読化解除 — エキスパート分析プレイブック
AI LOAD INSTRUCTION:ネイティブバイナリのコード難読化を特定、分類し、無効化するためのエキスパート向けテクニックです。ジャンクコード、不透明述語、SMC、制御フローのフラット化、movfuscator、VM プロテクター(VMProtect/Themida/Code Virtualizer)、文字列暗号化、インポート隠蔽、逆アセンブル対策のトリックを扱います。ベースモデルは、パッキングと難読化を混同しがちで、静的な難読化解除戦略と動的な難読化解除戦略の違いを見落とすことがあります。
0. 関連するルーティング
- 難読化されたバイナリにアンチデバッグ層もある場合は、anti-debugging-techniques
- 自動難読化解除に angr/Z3 を使用する場合は、symbolic-execution-tools
- VM プロテクターのバイトコードを詳しく分析する場合は、vm-and-bytecode-reverse
すばやく見分けるポイント
| IDA/Ghidra での症状 | 考えられる難読化 | 最初に試すこと |
|---|---|---|
| 平坦な CFG、大きな単一の switch | 制御フローのフラット化 | シンボリック実行で CFG を復元 |
mov 命令のみ | movfuscator | 難読化解除/トレースベースのリフト |
| pushad/pushfd → VM エントリ | VM プロテクター | ハンドラーテーブルの抽出 |
| コード実行前の XOR ループ | SMC/文字列暗号化 | 動的解析、復号後にブレークポイントを設定 |
| 不可能な条件(不透明述語) | ジャンクコードの挿入 | パターンベースの除去 |
| すべての文字列が読めない | 文字列暗号化 | 復号ルーチンにフックするか、エミュレート |
| IAT にインポートがない | インポート隠蔽 | GetProcAddress/ハッシュ解決をトレース |
1. ジャンクコードと不透明述語
1.1 ジャンクコードの挿入
プログラムの出力に影響しないデッドコードを追加し、解析にかかる時間を増やします。
識別方法:
- レジスターやメモリーに書き込むが、その後読み取られない命令
- 戻り値が破棄され、副作用もない関数呼び出し
- 使用されない結果を計算する、不変の境界値を持つループ
除去戦略:
- 定義・使用チェーンを計算します(IDA/Ghidra のデータフロー解析)
- 後続で使用されない命令をデッドコードとしてマークします
- 除去によってプログラムの動作が変わらないことを確認します(トレース比較)
1.2 不透明述語
条件が常に真または常に偽であるにもかかわらず、それが明白ではない条件分岐です。
| 種類 | 例 | 常に評価される値 |
|---|---|---|
| 算術 | x² ≥ 0 | 真 |
| 数論 | x*(x+1) % 2 == 0 | 真(連続する整数の積) |
| ポインタベース | エイリアス後の ptr == ptr | 真 |
| ハッシュベース | CRC32(constant) == known_value | 真 |
難読化解除:
- 抽象解釈:条件が定数であることを証明します
- シンボリック実行:Z3 で
∀x: predicate(x) = Trueを証明します - パターンマッチング:既知の不透明述語のパターンを認識します
- 動的解析:トレースして、分岐が実行されない/常に実行されることを確認します
import z3
x = z3.BitVec('x', 32)
s = z3.Solver()
s.add(x * (x + 1) % 2 != 0)
print(s.check()) # unsat → always true
2. 自己書き換えコード(SMC)
実行時にコードを書き換えます。暗号化されたコードは、実行直前に復号されます。
2.1 XOR 復号ループ(最も一般的)
lea esi, [encrypted_code]
mov ecx, code_length
mov al, xor_key
decrypt_loop:
xor byte [esi], al
inc esi
loop decrypt_loop
jmp encrypted_code ; now decrypted
2.2 分析戦略
1. Identify the decryption routine (look for XOR/ADD/SUB in loops writing to .text)
2. Set breakpoint AFTER the loop completes
3. At breakpoint: dump the decrypted memory region
4. Re-analyze the dumped code in IDA/Ghidra
5. For multi-layer: repeat for each decryption stage
2.3 エミュレーションによる自動アンパック
from unicorn import *
from unicorn.x86_const import *
mu = Uc(UC_ARCH_X86, UC_MODE_32)
mu.mem_map(0x400000, 0x10000)
mu.mem_write(0x400000, binary_code)
mu.emu_start(decrypt_entry, decrypt_end)
decrypted = mu.mem_read(code_start, code_length)
3. 制御フローのフラット化(CFF)
3.1 構造
元の逐次ブロックは、ディスパッチャーループに変換されます。
Original: A → B → C → D
Flattened: ┌──────────────────┐
│ dispatcher │
│ switch(state) │◄─────┐
├──────────────────┤ │
│ case 1: block A │──────┤
│ case 2: block B │──────┤
│ case 3: block C │──────┤
│ case 4: block D │──────┘
└──────────────────┘
各ブロックは、ディスパッチャーに戻る前に state = next_state を設定します。
3.2 復元テクニック
| テクニック | ツール | 有効性 |
|---|---|---|
| シンボリック実行 | angr、Triton、miasm | 高 — すべての状態遷移をトレースします |
| トレースベースの復元 | Pin/DynamoRIO のトレース → CFG を再構築 | 中 — 実行されたパスのみをカバーします |
| パターンマッチング | カスタム IDA/Ghidra スクリプト | 中 — 既知のフラット化手法に有効です |
| D-810(IDA プラグイン) | IDA Pro | 高 — CFF 専用に設計されています |
3.3 シンボリックなデフラット化(angr の手法)
import angr, claripy
proj = angr.Project('./obfuscated')
cfg = proj.analyses.CFGFast()
# Find dispatcher block (highest in-degree basic block)
dispatcher = max(cfg.graph.nodes(), key=lambda n: cfg.graph.in_degree(n))
# For each case block, symbolically determine successor
for block in case_blocks:
state = proj.factory.blank_state(addr=block.addr)
# ... solve state variable to find real successor
4. MOVFUSCATOR
4.1 概要
すべての計算を mov 命令のみに還元します(メモリーマップされた計算テーブルを介してチューリング完全です)。Christopher Domas が作成しました。
4.2 識別
- 関数に含まれるのは
mov命令のみです(add、sub、xor、jmp、callは含まれません) - データセクションに大きなルックアップテーブルがあります
- メモリマップドフラグレジスターがあります
4.3 デモブフスケーション
| アプローチ | 説明 |
|---|---|
| demovfuscator(ツール) | 静的解析を行い、movパターンから元の操作を復元します |
| トレース+テイント解析 | Pin/DynamoRIOで実行し、入力にテイントを設定して計算を観察します |
| シンボリック実行 | 関数全体を制約システムとして扱います |
5. VM 保護(VMProtect / Themida / Code Virtualizer)
5.1 VM アーキテクチャ
Protected code → bytecode compiler → custom bytecode
Runtime: VM entry (pushad/pushfd) → fetch → decode → execute → VM exit (popad/popfd)
5.2 VM エントリーポイントの識別
; Typical VMProtect entry
pushad ; save all registers
pushfd ; save flags
mov ebp, esp ; VM stack frame
sub esp, VM_LOCALS_SIZE ; allocate VM context
mov esi, bytecode_addr ; bytecode instruction pointer
jmp vm_dispatcher ; enter VM loop
5.3 ハンドラーテーブルの抽出
1. Find dispatcher (large switch or indirect jump via table)
2. Each case/entry = one VM handler (implements one VM opcode)
3. Map handler addresses to operations by analyzing each handler:
- Handler reads operand from bytecode stream (esi)
- Performs operation on VM registers/stack
- Advances bytecode pointer
- Returns to dispatcher
5.4 デバーチャライゼーションのアプローチ
| 方法 | 説明 | ツール |
|---|---|---|
| 手動でのハンドラーマッピング | 各ハンドラーをリバースエンジニアリングし、ISA仕様を作成します | IDA + スクリプト |
| トレース記録 | すべてのハンドラー実行を記録し、プログラムを再構成します | REVEN、Pin |
| シンボリックリフティング | ハンドラーをシンボリック実行し、IRにリフトします | Triton、miasm |
| パターンマッチング | ハンドラーパターンを既知のVMファミリーと照合します | カスタムスクリプト |
5.5 VMProtect の詳細
- ディスパッチャーで不透明述語を使用します
- ハンドラーの変更:同じオペコードでも、ビルドごとに異なるハンドラーコードになります
- 複数のVMレイヤー(VM内のVM)
- アンチデバッグ機能と整合性チェックを統合します
6. 文字列の暗号化
6.1 一般的なパターン
| パターン | 例 | 復元方法 |
|---|---|---|
| XORループ | for (i=0; i<len; i++) s[i] ^= key; | XOR関数をフックまたはエミュレートします |
| スタック文字列 | mov [esp+0], 'H'; mov [esp+1], 'e'; ... | IDA FLIRT / Ghidraスクリプトで再構成します |
| RC4暗号化 | 暗号化されたブロブ+バイナリ内のRC4キー | キーを抽出し、オフラインで復号します |
| AES暗号化 | 暗号化されたブロブ+実行時に導出されるAESキー | 復号後にフックします |
| カスタムエンコーディング | Base64 + XOR + 反転 | デコード関数をトレースして再現します |
6.2 文字列の自動復号
# Ghidra script: find XOR decryption calls, emulate them
from ghidra.program.model.symbol import SourceType
decrypt_func = getFunction("decrypt_string")
refs = getReferencesTo(decrypt_func.getEntryPoint())
for ref in refs:
call_addr = ref.getFromAddress()
# extract arguments (encrypted buffer ptr, key, length)
# emulate decryption, add comment with plaintext
7. インポートの隠蔽
7.1 GetProcAddress +ハッシュ検索
FARPROC resolve(DWORD hash) {
// Walk PEB → LDR → InMemoryOrderModuleList
// For each DLL, walk export table
// Hash each export name, compare with target hash
// Return matching function pointer
}
7.2 復元
- ハッシュアルゴリズムを特定します(一般的なもの:CRC32、djb2、ROR13+ADD)
- 既知のすべてのAPI名についてハッシュを計算します
- ハッシュ → API名のルックアップテーブルを作成します
- IDA/Ghidraで解決された呼び出しに注釈を付けます
7.3 一般的なハッシュアルゴリズム
| 名前 | アルゴリズム | 使用例 |
|---|---|---|
| ROR13 | hash = (hash >> 13 | hash << 19) + char | Metasploitのシェルコード |
| djb2 | hash = hash * 33 + char | さまざまなマルウェア |
| CRC32 | 関数名の標準CRC32 | 高度なパッカー |
| FNV-1a | hash = (hash ^ char) * 0x01000193 | 最新のマルウェア |
8. 逆アセンブルを妨害する手法
8.1 手法
| 手法 | 仕組み | 修正方法 |
|---|---|---|
| 命令の重複 | jmp $+2; db 0xE8(偽のcallプレフィックス) | 正しいオフセットから手動で再解析します |
| アラインメントがずれたジャンプ | 複数バイト命令の途中にジャンプします | IDAにターゲット位置から再解析させます |
| 条件付きジャンプのペア | jz $+5; jnz $+3(常にジャンプし、線形逆アセンブルを混乱させます) | 無条件のjmpに変換します |
| 戻りアドレスの操作 | jmp addrの代わりにpush addr; retを使用します | push+retをジャンプとして認識します |
| 例外ベースのフロー | 例外を発生させ、ハンドラー内に実際のコードを配置します | 例外ハンドラーチェーンを解析します |
| Call + add [esp] | call $+5; add [esp], N; ret(計算されたジャンプ) | 実際のターゲットを計算します |
8.2 IDA の修正
Right-click → Undefine (U)
Right-click → Code (C) at correct offset
Edit → Patch → Assemble (for permanent fix)
9. 判断ツリー
Obfuscated binary — how to approach?
│
├─ Can you run it?
│ ├─ Yes → Dynamic analysis first
│ │ ├─ Set BP on interesting APIs (file, network, crypto)
│ │ ├─ Trace execution to understand real behavior
│ │ └─ Dump decrypted code/strings at runtime
│ │
│ └─ No (embedded/firmware/exotic arch) → Static only
│ └─ Identify obfuscation type from patterns below
│
├─ What does the code look like?
│ │
│ ├─ Giant flat switch/dispatcher loop?
│ │ ├─ State variable drives control flow → CFF
│ │ │ └─ Use D-810 or symbolic deflattening
│ │ └─ Bytecode fetch-decode-execute → VM protection
│ │ └─ Extract handlers, build disassembler
│ │
│ ├─ Only mov instructions?
│ │ └─ movfuscator → demovfuscator tool
│ │
│ ├─ XOR/ADD loop writing to .text section?
│ │ └─ SMC → breakpoint after decode, dump
│ │
│ ├─ Impossible conditions in branches?
│ │ └─ Opaque predicates → Z3 proving or pattern removal
│ │
│ ├─ Disassembly looks wrong / functions overlap?
│ │ └─ Anti-disassembly → manual re-analysis at correct offsets
│ │
│ ├─ No readable strings?
│ │ └─ String encryption → hook decrypt function or emulate
│ │
│ ├─ No imports in IAT?
│ │ └─ Import hiding → identify hash, build lookup table
│ │
│ └─ pushad/pushfd → complex code → popad/popfd?
│ └─ VM protector entry/exit → full VM analysis
│
└─ What tool to use?
├─ Known protector (VMProtect/Themida) → specific deprotection guide
├─ Custom obfuscation → combine: IDA scripting + Triton + manual
├─ CTF challenge → angr symbolic execution often fastest
└─ Malware analysis → dynamic (debugger + API monitor) first
10. ツールボックス
| ツール | 用途 | 最適な用途 |
|---|---|---|
| IDA Pro + Hex-Rays | 逆アセンブル、逆コンパイル、スクリプト作成 | 幅広い分析 |
| Ghidra | スクリプト作成(Java/Python)に対応した無料の代替ツール | 低予算でのリバースエンジニアリング |
| D-810(IDA プラグイン) | CFF の自動フラット化解除 | OLLVM 形式の難読化 |
| miasm | IR ベースの分析フレームワーク | シンボリックな難読化解除 |
| Triton | 動的シンボリック実行 | 不透明述語の解決、CFF |
| REVEN | システム全体のトレース記録と再生 | VM プロテクターの分析 |
| demovfuscator | movfuscator のリバース | mov のみのバイナリ |
| x64dbg + プラグイン | スクリプト作成に対応した動的分析 | Windows のリバースエンジニアリング |
| Unicorn Engine | CPU エミュレーション | SMC のアンパック、シェルコード |
| Capstone | 逆アセンブルライブラリ | カスタムツール |
| IDA FLIRT | 関数シグネチャの照合 | シンボルを削除したバイナリ内のライブラリコードの特定 |
| Binary Ninja | MLIL/HLIL に対応した代替逆アセンブラー | 自動分析 |