概要
参考:https://www.ndss-symposium.org/wp-content/uploads/bar2026-58.pdf
本記事では、CTFのReverse Engineering問題を対象に、LLM Agentがどこまで自律的に静的解析・解答提出まで到達できるかを評価するベンチマークを作成し、その結果を整理する。
ベンチマークには2025年に開催された複数のCTFからReverse Engineering問題を16問収録した。AgentはOpenCodeから操作され、GhidraMCPに加えて file / strings / readelf / objdump / nm / Python / Z3などの静的解析ツールを利用できる。一方で、対象バイナリの実行は禁止している。単にFlagを推測できるかではなく、バイナリ構造の把握、必要な解析手段の選択、複数ファイルの関連付け、制約復元、最終的な提出操作まで含めたAgent性能を測ることを目的とした。
今回の結果ダッシュボードでは、主に以下の6問を使って15モデルを比較している。
- #01
baby-goes-re - #04
what - #06
tagme - #07
debugalyzer - #08
Pain Is Justice - #19
constructor
なお、OpenRouterについてはモデル指定以外の設定はデフォルト設定とし、特定providerへの速度最適化や固定routingを行わない条件で比較した。
ベンチの説明
実行環境情報
Benchmark : Static Reverse-Engineering Benchmark v1.8
収録問題数 : 16
今回の比較対象 : 主に6問
Agent : OpenCode 1.18.23
LLM API : OpenRouter
OpenRouter設定 : デフォルト
Reverse Engineering : GhidraMCP + CLI static-analysis tools
GhidraMCP : mecha_ghidra @ 7a8d560847c9cbb6f79fc52858de13d3ce414e7e
Runner : Node.js 22.18.0 / Debian Bookworm
Grader : Python 3.13.7 / Debian Bookworm
実行基盤 : Docker Compose
対象バイナリ実行 : 禁止
Graderによるバイナリ実行: なし
初期LLM API Call上限 : 30 successful calls / task
1回目REJECT後 : +15 successful calls
最大提出回数 : 2
Task timeout : 2000 sec
最終Call後Grace : 15 sec
OpenCode出力上限 : min(262144, model max completion tokens)
provider側の 429、5xx、stream failure、client側の途中切断など、正常なモデル応答として完了していないリクエストはsuccessful callとして数えない。これにより、provider障害をモデルの推論性能と混同しにくくしている。
また、OpenCodeが正しいFlagを通常テキストとして出しただけで submit_flag を実行しなかった場合などは救済しない。ツール利用や最終提出まで含めてAgent性能として評価する。
収録問題
ベンチ本体には以下の16問を収録している。
| ID | Challenge | Event |
|---|---|---|
| 01 | baby-goes-re | justCTF 2025 |
| 02 | Ez Flag Checker | SECCON CTF 14 Quals |
| 03 | Multiarch-1 | Google CTF 2025 |
| 04 | what | b01lers CTF 2025 |
| 05 | Crown Flash | SECCON CTF 14 Quals |
| 06 | tagme | corCTF 2025 |
| 07 | debugalyzer | DiceCTF Quals 2025 |
| 08 | Pain Is Justice | ASIS CTF Quals 2025 |
| 09 | oño | DiceCTF Quals 2025 |
| 11 | Jormugandr | KalmarCTF 2025 |
| 12 | 1Zwasm | 0CTF 2025 |
| 14 | labyrinth | b01lers CTF 2025 |
| 15 | pipe-dream | b01lers CTF 2025 |
| 16 | What In Ternation | SekaiCTF 2025 |
| 17 | bubble | corCTF 2025 |
| 19 | constructor | idekCTF 2025 |
今回の結果ダッシュボードでは、このうち #01 / #04 / #06 / #07 / #08 / #19 を中心に比較した。
各問題のカテゴリとチェックポイント
以下の「チェックポイント」は部分点ではなく、Agentが解答へ到達するために必要となる代表的な解析地点を整理したものである。現行graderは基本的に最終提出が ACCEPTED になった場合のみPASSとする。
| ID | Challenge | カテゴリ | 代表的なチェックポイント | 最終判定 |
|---|---|---|---|---|
| 01 | baby-goes-re | Go / 基本静的解析 / 埋め込みデータ参照 | 64-bit static Go ELFを認識 → main.main / main.CheckFlag を特定 → 入力長 0x35 = 53文字 のチェックを発見 → CheckFlag 内のindex依存比較を復元 |
exact flag |
| 04 | what | 独自インタプリタ / 算術状態機械 | PIE ELFの main を解析 → .rodata の命令列 972 bytes を特定 → ? が 61個、比較命令 T も 61個 → W/H/A と WHAT テーブルによる演算を逆算 |
exact flag |
| 06 | tagme | table-driven state machine / 文字制約 | stripped PIEからmainを復元 → getline長が 9~39 bytes(改行込み) → corctf{ と }\n を確認 → 中間部最大 30文字 → 10-entry tableと位置依存の文字制約・状態遷移を解析 |
exact flag |
| 07 | debugalyzer | DWARF / Debug Info / 複数ファイル解析 |
main と dwarf の2ファイル関係を把握 → dwarf が .debug_line を読むことを特定 → execute_dwarf_bytecode_v4 を解析 → main のDWARF v4 line programからFlag生成ロジックを復元 |
exact flag |
| 08 | Pain Is Justice | 制約充足 / Kakuro / accepted-input | 9×9 Kakuroを復元 → playable cell 47個 → 各値は 1~9 → 横・縦runで「重複なし+clue合計一致」 → 47数字をrow-major・空白区切りで提出 | semantic accepted-input |
| 19 | constructor |
.init_array / constructor / pre-main処理 |
stripped static ELFでmain以外を調査 → .init_array に 3 entries → 非自明constructor 0x401050 を発見 → .rodata 0x403040 から 0x2a = 42 bytes を復号 → pre-main比較ロジックを復元 |
exact flag |
結果
結果ダッシュボードは15モデル・86件の採用model/task recordから構成されている。比較対象6問をすべて持つモデルでは、openai/gpt-5.6-sol と qwen/qwen3.8-flash が6/6に到達した。一方で、同じ6/6でも採用ログの総tokenは約1.84Mと約3.61Mで差があり、正答率だけでなく解析効率にも大きな差が見える。問題別では #06 tagme のPASS率が3/14、#08 Pain Is Justice が5/14と低く、単純な静的解析よりも状態遷移や制約問題で差が広がった。逆に #19 constructor は12/14、#04 what は12/15がPASSしている。なお一部モデルは6問すべての記録がないため、単純な総得点比較ではなく問題別結果とtoken消費を併せて見る必要がある。
ベンチ作成Step
ベンチ作成は一度で完成したわけではなく、実際のモデルを流しながらログを確認し、問題を1つずつ修正していった。
| Step | 作業 | 代表的な指示 |
|---|---|---|
| 1 | CTF Reverse Engineering問題を収集し、固定dataset化 | 「バイナリとFlagを収集してベンチ化したい」 |
| 2 | Agent用の静的解析ルールを作成 | 「対象ファイルの実行は禁止。GhidraMCPや静的解析ツールを使わせたい」 |
| 3 | 長すぎるpromptを整理 | 「プロンプトが長いです。重複している指示を分析し削除して下さい」 |
| 4 | Graderと再提出フローを実装 | 「Reject時には、方針を変更するか別の可能性を検証して再提出させたい」 |
| 5 | accepted-input型問題へ対応 | Task 8などを「Flag文字列比較ではなく、正しい入力なら通る」方式へ変更 |
| 6 | GhidraMCP bootstrapを修正 | main誤検出、PIEの +0x100000、stale projectなどをログから切り分け |
| 7 | OpenRouter proxyとcall budgetを追加 | provider 429/5xxをAgent失敗として数えないように修正 |
| 8 | Streaming / BrokenPipeを修正 | 「なんかおかしい」とログを確認し、bufferingからSSE逐次転送へ変更 |
| 9 | Monitor・結果可視化を作成 | 「このように可視化スクリプトを作って」「同じScore・失敗系なら最新ログを採用して」 |
| 10 | OpenCode固有挙動を修正 | hidden title generation callを --title 指定で除去 |
| 11 | 32K output ceilingを修正 | 「256kまでは止まらないようにしたいな」 |
| 12 | Agent失敗とHarness救済の境界を決定 | 「間違った提出方法は取りこぼしてよいです。Agent性能込みのベンチなので」 |
| 13 | モデル/provider差を確認 | 「Qwen3.8-27Bの最適なプロバイダOpenrouter探して。Jsonも作って」などで検証 |
| 14 | v1.8として固定 | 「これが修正が必要なファイルです。完了後はv1.8としてください」 |
作成途中ではprovider固定routingも検証したが、今回の記事で掲載する比較結果ではOpenRouter設定はデフォルトとしている。
ベンチ作成時の課題
-
GhidraMCPの解析状態が残る
前回Taskのprojectが混入するケースがあり、containerを--force-recreateする必要があった。 -
PIEのアドレスがGhidraとELFでずれる
ET_DYNではGhidra側のimage base0x100000を考慮する必要があった。 -
mainのsubstring誤検出
__libc_start_mainをmainとして拾うことがあり、完全一致に変更した。 -
Taskによって採点方法が異なる
exact flagだけではTask 8などを公平に評価できず、accepted-input graderを追加した。 -
grader側のreference answer typo
正しいAgent出力がREJECTされるケースがあり、graderデータ自体の検証も必要だった。 -
OpenRouterの429/5xxとモデル失敗の分離
provider障害まで30-call budgetへ加算すると評価が不安定になるため、successful callのみ数える方式へ変更した。 -
ProxyのSSE buffering / BrokenPipe
長い推論でresponseをbufferすると停止して見えるため、HTTP/1.1 chunked streamingへ変更した。 -
OpenCodeのhidden title generation call
solver以外のLLM callが混ざっていたため、固定--titleを指定した。 -
OpenCode 1.18.23の32K output ceiling
reason=lengthで解析途中に終了するため、最大262,144 tokensまで引き上げた。 -
NO_ANSWERの原因が複数存在
provider障害、call limit、正常stop、malformed tool call、output lengthなどをログから個別に分類する必要があった。 -
正解をテキストで出しても提出しないAgentが存在
自動救済するとAgent性能を歪めるため、そのままNO_ANSWERとした。 -
token消費量のばらつきが大きい
同じPASSでも数倍以上tokenを使うモデルがあり、Scoreとtokenの両方を可視化する必要があった。
まとめ
Reverse Engineering Agentの評価では、最終的なFlag正答率だけを見ると、モデルが実際にどこで失敗したのかが分かりにくい。今回のベンチでは、GhidraMCPによる初期解析、静的解析ツールの利用、LLM API call budget、graderへの提出、provider障害の分離までを一つのHarnessとして管理した。
結果を見ると、単純な問題では多くのモデルが解ける一方、table-driven state machine、DWARF、Kakuroのように「解析結果を別の表現へ落とし込み、継続的に検証する」必要がある問題で差が出やすかった。また、同じ正答数でもtoken消費には大きな差があり、Reverse Engineering Agentを比較する際には正答率・Agentの完走能力・tool-callの正確さ・token効率を合わせて見る必要があると感じた。
