
PE/ELF x86/x64 CFG 抽出ツール。バイナリを受け取り、逆アセンブルし、すべてのジャンプとコール (GOT、ジャンプテーブル、レジスタトレーシング) を解決し、完全な制御フローグラフを構造化 JSON としてエクスポートします。
すべての分岐がどこへ向かうのかを正確に知る必要があります。読むためではなく、パッチを当てるためです。JSON を Zydis や AsmJit に入力し、フックや変更が必要な正確な命令を特定し、書き戻します。アンチチートチームはこれを使用してゲームバイナリのマッピングを行います。リバースエンジニアリング担当者は、コードを自身の分析パイプラインに持ち上げるために使用します。ソフトウェアアナリストは、バイナリを実行せずに実行パスをトレースします。
cfgrip はマップを提供します。それをどう使うかはあなた次第です。
cfgrip が処理するすべてのバイナリに対して、次のものを生成します。
.pdata から)、名前 (エントリポイント、エクスポート、検出)、およびサンクタグ (PLT スタブ) を含む.pdata エントリ、データセクション関数ポインタ)--clean を指定すると、次の追加処理を行います。
jmp → jmp チェーンを直接エッジに変換stack_offset 注釈 (関数エントリからの RSP 差分)xrefs セクションの構築:すべてのコール/ジャンプターゲットを呼び出し元にマッピングまた、間接分岐は次の方法で処理します。
mov/lea チェーンを逆方向にたどって具体的なアドレスを特定cfgrip [--subs-only] [--clean] <binary>
バイナリを入力すると、<binary>.cfg が出力されます。
--subs-only を使用すると、エントリポイントからコールグラフ経由で到達可能な関数のみを抽出します。到達不能なエクスポートやプロローグ候補はスキップされます。
--clean を使用すると、ジャンプスレッディング、デッドブロック除去、スタックオフセット追跡、クロスリファレンス分析が適用されます。
.cfg ファイルは構造化 JSON です。以下がその外観です:
{
"binary": "tests\\example1.exe",
"mode": "full",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [
{
"address": "0x140020000",
"name": "EncodePointer",
"library": "KERNEL32.dll"
},
{
"address": "0x140020008",
"name": "DecodePointer",
"library": "KERNEL32.dll"
},
{
"address": "0x140020010",
"name": "EnterCriticalSection",
"library": "KERNEL32.dll"
},
{
"address": "0x140020018",
"name": "LeaveCriticalSection",
"library": "KERNEL32.dll"
},
{
"address": "0x140020020",
"name": "InitializeCriticalSectionEx",
"library": "KERNEL32.dll"
},
...
...
...
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [
{
"address": "0x1400054bc",
"size": 4,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30"
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340"
}
],
"successors": [
"0x140005340"
]
},
{
"address": "0x140005340",
"size": 8,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x140005340",
"size": 5,
"mnemonic": "mov",
"operands": "qword ptr [rsp + 8], rbx"
},
{
"address": "0x140005345",
"size": 5,
"mnemonic": "mov",
"operands": "qword ptr [rsp + 0x10], rsi"
},
{
"address": "0x14000534a",
"size": 1,
"mnemonic": "push",
"operands": "rdi"
},
{
"address": "0x14000534b",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x30"
},
{
"address": "0x14000534f",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 1"
},
{
"address": "0x140005354",
"size": 5,
"mnemonic": "call",
"operands": "0x14000550c"
},
{
"address": "0x140005359",
"size": 2,
"mnemonic": "test",
"operands": "al, al"
},
{
"address": "0x14000535b",
"size": 6,
"mnemonic": "je",
"operands": "0x140005497"
}
],
"successors": [
"0x140005497",
"0x140005361"
]
},
{
"address": "0x140005497",
"size": 15,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x140005497",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 7"
},
{
"address": "0x14000549c",
"size": 5,
"mnemonic": "call",
"operands": "0x140005e44"
},
{
"address": "0x1400054a1",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054a2",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 7"
},
{
"address": "0x1400054a7",
"size": 5,
"mnemonic": "call",
"operands": "0x140005e44"
},
{
"address": "0x1400054ac",
"size": 2,
"mnemonic": "mov",
"operands": "ecx, ebx"
},
{
"address": "0x1400054ae",
"size": 5,
"mnemonic": "call",
"operands": "0x14000ec14"
},
{
"address": "0x1400054b3",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054b4",
"size": 2,
"mnemonic": "mov",
"operands": "ecx, ebx"
},
{
"address": "0x1400054b6",
"size": 5,
"mnemonic": "call",
"operands": "0x14000ebcc"
},
{
"address": "0x1400054bb",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30"
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340"
}
],
"successors": [
"0x140005340"
]
},
...
...
...
各関数にはオプションフィールドが含まれるようになりました:
end_address — 利用可能な場合の正確な関数終了アドレス (PE .pdata 例外テーブルから)。それ以外の場合は、すべてのブロックの最大命令アドレスとして計算されます。is_thunk — PLT スタブおよびインポートサンク (別のアドレスにリダイレクトするだけの関数) の場合に true。--subs-only 出力{
"binary": "C:\\binaries\\target.exe",
"mode": "subs-only",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [ ... ],
"indirect_targets": [ ... ],
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [ ... ]
},
...
]
}
"mode": "subs-only" フィールドは、ダウンストリームツールにこの CFG がエントリポイントから到達可能な関数のみを含むことを伝えます。到達不能なエクスポートとプロローグ候補は除外されます。関数が少なくなり、分析対象がよりクリーンになります。
--clean 出力{
"binary": "C:\\binaries\\target.exe",
"mode": "full+clean",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [ ... ],
"indirect_targets": [ ... ],
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [
{
"address": "0x1400054bc",
"size": 4,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28",
"stack_offset": 0
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30",
"stack_offset": -40
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28",
"stack_offset": -40
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340",
"stack_offset": 0
}
],
"successors": [ "0x140005340" ]
},
...
]
},
...
],
"xrefs": [
{
"target": "0x140011b00",
"callers": [
{ "address": "0x14001a30f", "type": "call" },
{ "address": "0x140019ec1", "type": "call" },
...
]
},
{
"target": "0x140007394",
"callers": [
{ "address": "0x14001a31a", "type": "call" },
...
]
},
...
]
}
--clean モードのすべての命令には stack_offset が含まれます — その命令ポイントにおける関数エントリからの RSP 差分です。xrefs セクションは、各コール/ジャンプターゲットをそれを参照するすべての命令にマッピングします。
--subs-only --clean 結合出力{
"binary": "C:\\binaries\\target.exe",
"mode": "subs-only+clean",
...
}
--clean と同じ構造ですが、"mode": "subs-only+clean" で両方のフィルタが適用されたことを示します。関数数はエントリポイントから到達可能な関数のみに削減され、残りの関数にはスタックオフセットと xrefs が含まれます。
cfgrip は複数の検出パスを通じて関数を発見します:
is_thunk: true の関数は PLT スタブまたはインポートサンクです。別のアドレスにリダイレクトする単一ブロックの関数です。
CMake と C++17 コンパイラが必要です。Capstone は自動的に取得されます。
cmake -B build
cmake --build build --config Release
./build/cfgrip <binary>
または Windows で Visual Studio を使用する場合:
cmake -B build -S .
cmake --build build --config Release
.\build\Release\cfgrip.exe <binary>

| | | |:-:|
| フォーマット | PE (32/64-bit) | YES |
| | ELF (64-bit) | YES |
| アーキテクチャ | x86 | YES |
| | x86-64 | YES |
| 間接コール | GOT resolution | YES |
| | Jump table detection | YES |
| | Backward register tracing | YES |
| 関数検出 | Entry point | YES |
| | Exports | YES |
| | call targets | YES |
| | Prolog scanning (MSVC x64, GCC, CET endbr64) | YES |
| | Tail-call detection (jmp → function) | YES |
| | PE .pdata (exception handler table) | YES |
| | Data-section function pointer scan | YES |
| サンク検出 | PLT stubs / import thunks (is_thunk) | YES |
| 関数境界 | end_address from .pdata or max instruction | YES |
| Subs-only モード | --subs-only flag | YES |
| CFG クリーニング | --clean (jump-thread, dead-block prune, stack deltas, xrefs) | YES |
このツールの関数境界検出は、"Function Boundary Detection in Stripped Binaries" (Alves-Foss & Song, 2019) で説明されているアプローチに基づいています。この論文では、 stripped x86/x64 バイナリで関数の開始と終了を見つけるためのマルチヒューリスティックアルゴリズムを導入しています。
論文は papers/Function_Boundary_Detection_in_Stripped_Binaries.pdf で入手できます。
論文のヒューリスティックへの実装のマッピング:
この論文の重要な洞察は、機械学習なしのアルゴリズム的ヒューリスティックがストリップされたバイナリで高い精度を達成できることです。当実装はこの哲学に従い、各パスが他のパスが見逃す可能性のある関数を捕捉するマルチパスアプローチを使用しています。
| パス | 検出内容 | カバレッジ |
|---|
| プロローグスキャン | push rbp, push r15/r14/r13/r12/rbx/rdi/rsi, sub rsp, >=0x20, enter | MSVC x64, GCC, リーフ関数, CET (endbr64) |
| コールターゲット | すべての call 命令のターゲットは関数開始点 | 直接コールおよび GOT 解決済み間接コール |
| テールコール | プロローグ候補をターゲットとする jmp 命令 | 最適化されたテールコールチェーン |
.pdata (PE) | 例外ハンドラテーブルからのランタイム関数エントリ | すべての x64 PE 関数の正確な開始/終了 |
| データポインタ | .rdata/.data 内の実行可能コードを指す 8 バイト値 | 関数ポインタ、vtable、コールバック |
| ヒューリスティック | 論文の説明 | 当実装 |
|---|
| H1–H4 | Prolog signatures (push rbp, callee-saved regs, stack sub, enter) | isProlog() in disasm/engine.cpp — push rbp, push r15..rbx, sub rsp >= 0x20, enter を検出 |
| H5 | Call-target seeding | すべての直接 call ターゲットは関数開始点 |
| H6 | Jump-to-function (tail call) detection | プロローグ候補への jmp はターゲットを関数キューに追加 |
| H7 | Exception table parsing | PE .pdata RUNTIME_FUNCTION エントリが正確な開始/終了を提供 |
| H8 | Data reference analysis | scanDataPointers() はデータセクションをスキャンしてコードポインタを探す |