0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

丸ベンチ_3step

0
Last updated at Posted at 2026-08-30

概要

参考:https://www.ndss-symposium.org/wp-content/uploads/bar2026-58.pdf

LLMにCTFのリバースエンジニアリング問題を解かせたとき、単純な「Flagを取れた / 取れなかった」だけでは、どこまで解析できていたのかが分かりません。特にpacker、custom VM、staged loader、WASMのような問題では、最終Flagまで届かなくても途中の解析は正しく進んでいることがあります。

そこで今回は、難度の高い3問をそれぞれ3フェーズに分割し、前フェーズをPASSしたモデルだけが次のフェーズへ進める静的リバースエンジニアリング用ベンチマークを作成しました。

対象は以下の3問です。

  • Google CTF 2025: Multiarch-1
  • DiceCTF Quals 2025: oño
  • 0CTF 2025: 1Zwasm

各フェーズでは、前フェーズで得られるべき解析結果をベンチ作者側で事前検証したcanonical handoffとして渡します。モデル自身の前フェーズの会話や推論文は引き継がず、全モデルが同じverified stateから次の解析を開始する設計です。

この方式にすることで、単純な最終Flag正解率ではなく、「unpackまではできた」「次段ELFまでは復元できた」「VM解析の途中で止まった」といった段階的な性能差を評価できます。

ベンチの説明

本ベンチはOpenCodeとGhidraMCPを利用した、静的解析限定のリバースエンジニアリングベンチマークです。

ターゲットバイナリの実行とWeb検索は禁止しています。モデルはGhidraMCP、binutils、Python、Z3などを利用して解析します。grader側もchallenge binaryを実行せず、中間checkpointは提出値のSHA-256比較、最終フェーズはFlagの完全一致で採点します。

フェーズは以下のように依存します。

3-1 -> 3-2 -> 3-3
9-1 -> 9-2 -> 9-3
12-1 -> 12-2 -> 12-3

前フェーズがPASSした場合のみ次フェーズがunlockされます。FAIL、CALL_LIMIT、NO_ANSWERなどの場合、後続フェーズはBLOCKEDとなり、モデル呼び出しもcanonical handoffの公開も行われません。

API call上限はフェーズごとに設定しています。

Phase 1: 16 calls
Phase 2: 14 calls
Phase 3: 14 calls
1回目のREJECT後: +8 calls

実行環境情報

Benchmark: v1.9 phased 3/9/12
Runtime: Docker Compose
Agent: OpenCode 1.18.23
Reverse Engineering: GhidraMCP / mecha_ghidra
GhidraMCP commit: 7a8d560847c9cbb6f79fc52858de13d3ce414e7e
Runner base: Node.js 22.18.0 Bookworm Slim
Grader: Python 3.13.7 Slim Bookworm
Provider: OpenRouter
OpenRouter provider routing: デフォルト(特定providerへの固定なし)
Target execution: 禁止
Web access: 禁止
Timeout: 2000秒 / phase
Phase API limits: P1=16 / P2=14 / P3=14
Retry budget after first REJECT: +8 calls

主な解析ツール:
GhidraMCP, file, strings, readelf, objdump, nm, objcopy,
addr2line, c++filt, xxd, od, rg, grep, sed, awk,
sha256sum, unzip, xz, Python, Z3

評価モデル:
openai/gpt-5.6-sol
qwen/qwen3.8-flash
qwen/qwen3.8-27b
z-ai/glm-5.3-flash

なお、OpenRouterのprovider routing自体はデフォルトで、特定providerへの固定は行っていません。ベンチ側のモデル設定として、Qwen 3.8 Flash / 27Bにはreasoning effort=mediumを指定しています。

各問題のカテゴリ

問題 CTF 主なカテゴリ ベンチで評価した能力
#3 Multiarch-1 Google CTF 2025 Custom VM / Multi-architecture emulator / VM bytecode 独自MASM形式の解析、StackVM/RegVMの理解、VM上の入力制約復元
#9 oño DiceCTF Quals 2025 Packer / Staged loader / ELF unpacking 多段loaderの静的unpack、exact byte復元、次段checker解析
#12 1Zwasm 0CTF 2025 Packer / Native loader / WebAssembly / Custom VM / Block cipher 多段unpack、native runtime復元、WASM抽出、checker/暗号ロジック解析

各ステップの到達条件

#3 Multiarch-1

multiarch が独自形式の crackme.masm を読み込み、複数のVM実行モデルを使って3段階のChallengeを検証する問題です。

Phase 1: Challenge 1

開始ファイル:

multiarch
crackme.masm

到達条件:

Challenge 1 response = 2405061754

このフェーズではMASMの基本構造と最初のVM経路を解析します。MASMは3セグメント構成で、segment headerは type, offset(uint16), size(uint16) として解釈できます。

Phase 2: Challenge 2

Phase 1 PASS後、Phase 1で確認済みのMASM/VM構造とresponseがcanonical handoffとして渡されます。

到達条件:

raw bytes = 46 91
submit value = 4691

提出値は文字列ではなく、生の2 byteを4桁lowercase hexに変換した値です。

Phase 3: Challenge 3

Phase 1/2のverified resultを固定し、後段のVM状態を解析します。

到達条件:

Challenge 3 response = 45483068

Phase 3では、StackVMから後段のRegVM側へ移る処理を含め、最後の入力条件を復元できるかを評価します。


#9 oño

複数のELFを順番に復元して次段へ制御を渡すstaged loader型の問題です。単にpackerを認識するだけでなく、生成される次段ELFをexact bytesで復元できるかをcheckpointにしています。

Phase 1: outer -> stage2

開始ファイル:

ono

到達条件:

stage2.elf size = 43,432 bytes
SHA-256 = ae9d7b3f4c1ed3348a18cfa8f9e75b38a4d3bc560cb2636425024d704b1aede6

outer stageの変換処理を静的に再実装し、最初にインメモリ実行されるELFを復元します。

Phase 2: stage2 -> stage3

Phase 1 PASS後、verified stage2.elf と実行コンテキストを配布します。

到達条件:

stage3.elf size = 28,976 bytes
SHA-256 = 5c909d9111991c434d5959ff69a37f84962d8154e7a7a903a4a76b8af1adccd0

Phase 1のunpackをやり直さず、stage2固有の変換処理だけを解析します。

Phase 3: final checker

Phase 2 PASS後、verified stage3.elf と累積handoffを配布し、残りのstageと最終checkerを解析します。


#12 1Zwasm

packed native binaryの内部からnative runtimeとWebAssembly moduleが生成され、最終的にWASM/custom VM/block cipher系のcheckerへ到達する問題です。

Phase 1: packed binary -> native chall

開始ファイル:

1Zwasm

到達条件:

chall size = 124,758 bytes
SHA-256 = 3fe655f7913d4e54f4f0b807f850baff54a063abeb2f2f0c37ea4f92241168fe

多段unpack処理を静的に解析し、実行時に生成されるnative ELFをexact bytesで復元します。

Phase 2: WebAssembly module recovery

Phase 1 PASS後、verified native runtime chall を配布します。

到達条件:

chall_wasm.wasm size = 87,226 bytes
SHA-256 = 04b8b074cf73fb37d6d4080732ca039aa4503e8d57e44b62224c8daa5f5115e2

作者側では、native runtimeとWASM moduleの組み合わせが元packed binaryと同じsuccess/failure判定を再現することを確認しています。

Phase 3: WASM checker / final Flag

packer層は解決済みとして、challchall_wasm.wasm のchecker semanticsを解析します。

結果

スクリーンショット_30-8-2026_15536_.jpeg

簡易分析

今回の結果では、4モデル合計で11/22 evaluated phaseがPASSしました。最も進んだのはGPT-5.6 Solで、#3と#9はいずれもPhase 2まで突破し、全体で4/7 evaluated PASSでした。Qwen3.8-FlashとGLM-5.3-Flashはともに#9でPhase 2まで到達し、全体傾向は近い一方、GLM-5.3-Flashの方が採用ログのtoken消費は少なめでした。#9は3モデルがPhase 3へ到達しており、中間unpackは比較的成功しやすい反面、最終checkerが明確な壁になっています。#3はモデル差が出やすく、GPT-5.6 SolだけがPhase 3まで到達しました。最も難しかったのは#12で、4モデルすべてがPhase 1を突破できず、いずれもnative chall の静的復元段階で停止しています。最終Flagだけを見ると全モデル未完走ですが、フェーズ化することで「どの解析境界まで到達できたか」という性能差を明確に観測できました。

ベンチ作成Step

  1. CTF問題群を収集し、実際に必要なファイル・Flag・accepted inputを確認した。
  2. モデル失敗とbenchmark/infrastructure失敗を切り分け、壊れている問題や不足resourceを除外・修正した。
  3. malware analysisとの関連が強く、静的解析で差が出やすい #3 / #9 / #12 に対象を絞った。
  4. 作者側で静的・動的解析を行い、各問題に自然なcheckpointが存在することを確認した。
  5. 各問題をP1/P2/P3へ分割し、中間到達条件を数値・hash・artifactとして固定した。
  6. 前フェーズPASS時にのみ次フェーズをunlockするdependency gateを実装した。
  7. モデルの過去会話ではなく、作者作成のcanonical verified handoffを次フェーズへ渡す方式にした。
  8. dataset/config/resultをsolverから隔離し、Ghidraにもcurrent phaseだけを公開するようにした。
  9. 後段phaseの直接起動や過去runからの情報漏洩を防止した。
  10. API call budgetをP1=16 / P2=14 / P3=14、初回REJECT後+8に調整した。
  11. 結果をphase progression、token、API callで比較できるdashboardを作成した。
  12. 同一model×challengeで複数runがある場合は、高いScoreを優先し、同Scoreなら最新ログを採用するよう可視化ロジックを調整した。

ベンチ作成時の課題

  • モデルの失敗と、grader・権限・provider・resource不足などのinfrastructure failureを区別する必要があった。
  • 初期版ではgraderがchallenge binaryを実行しており、「静的解析限定」という条件と矛盾していた。
  • dataset/config/resultや他phaseのartifactがsolverから見え、未来のcheckpointが漏洩する可能性があった。
  • Ghidraへ全sampleをmountすると、別phaseや過去projectが暗黙のmemory channelになる問題があった。
  • 最初のphase設計では、前phaseの「答え」だけを渡しており、その値が何を意味するのかモデルが理解できない問題があった。
  • 逆にcurrent phaseのstrategyを詳しく書きすぎると、解法そのものをbenchmark側が教えてしまう問題があった。
  • dependency gateが無い状態では、前phase FAIL後でも後段handoffを取得できてしまった。
  • --only で後段phaseを直接起動するとcheckpointを迂回できるため、通常runでは禁止する必要があった。
  • 9個のphaseを独立問題として採点すると、前段FAIL・後段PASSのような不可能な進行を表現できてしまった。
  • API call上限をconfigとrunnerの両方に固定値として持たせたことで、設定変更時に不整合で起動停止する問題が発生した。
  • dashboard初期版ではrun単位の選択が強すぎ、同一モデルの#3/#9/#12のbest resultを正しく併記できなかった。
  • BLOCKED を通常のFAILと同じ分母へ入れると、実際に評価されていないphaseまで失敗扱いに見える問題があった。
  • provider障害やHTTP 5xxをモデル能力の失敗として扱わないためのログ・status設計が必要だった。

ベンチ作成時に重視した指示

  • ターゲット実行は禁止し、静的解析だけで評価する。
  • 難しい問題はall-or-nothingにせず、3フェーズへ分割して途中到達度を測る。
  • 前フェーズをPASSした場合だけ次フェーズを解放する。
  • 次フェーズには、モデルの生の会話ではなく作者が事前検証したcanonical analysisを渡す。
  • 前phaseの結果だけでなく、その値の意味、解析済み構造、必要なartifact/contextもhandoffする。
  • solverから他phaseのdataset/config/resultを見えなくする。
  • Ghidraにはcurrent phaseだけを見せる。
  • 後段phaseだけを直接実行してhandoffを取得する抜け道を作らない。
  • 採点は9個の独立問題ではなく、#3/#9/#12それぞれのP1→P2→P3到達度として扱う。
  • API call budgetはP1=16 / P2=14 / P3=14、初回REJECT後は+8とする。
  • OpenRouterは特定providerへ固定せず、provider routingはデフォルトで実行する。
  • dashboardでは基本的に高いScoreを採用し、Scoreが同じ、または同程度の失敗結果なら最新ログを採用する。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?