Ghidra における反復的リバースエンジニアリング
Ghidra は、あなたが読み取るための逆アセンブリビューアではありません。これは、永続的でバージョン管理されたデータベースを備えた推論エンジンです。あなたが適用した知識は、デコンパイラが次に表示する内容を変化させ、そこから次の知識が生まれます。データベースへの書き込みを拒否することは、ツールの主要なメカニズムを捨て去り、より高コストな選択となります。
しかし、このループにはバッチ分析にはない失敗モードがあります。それは、自分自身の推測を確証できてしまうことです。 推論を適用し、再読込すると、確認のための読み取りは独立しているように見えますが、実際はそうではありません。このファイルは、各ラウンドを拘束するループとルールです。その背後にあるカタログは references/ にあり、あなたが行おうとしている作業の種類ごとにキー付けされています。
確信度の表記規則。 (doc) と記された主張は Ghidra の公式ドキュメントに由来しますが、実行は確認されていません。記されていない主張は、実際に測定されたものであるか、プロジェクトの履歴に基づいています。あるバイナリからの引用数値は、調整のための例であり、あなたのバイナリの特性ではありません。
ここから始める
以下のループは apply から始まりますが、開いたばかりのバイナリにはまだ適用するものは何もありません。 このファイルにたどり着くには2つの異なる経路があり、それぞれ最初のアクションが異なります。先に進む前に、自分の状況を見つけてください。
A — まだ誰も解析していないバイナリ。 ループに入らないでください。ラウンドゼロは推論ではなく、測定です。
- 何を見ているのか確認する。 ファイル自体から、アーキテクチャ、エンディアン、コンパイラツールチェーンを確認します。インポート時にエンディアンを間違えると、逆アセンブリ全体が破損し、後の解析では回復できません(
references/platforms-eras.md)。マネージドエンジンやスクリプトエンジンの場合は、立ち止まって確認してください。ダンパーを使えば、数ヶ月の逆アセンブリでは得られない情報を1分で出力できるかもしれません(references/game-recon.md)。 - 単一の関数を解釈する前に、以下の free-names 列全体を実行する。 各行は、あなたのバイナリに関する仮説であり、単一のコマンドでテストできます。測定されたゼロは発見です。存在しない情報源を前提にフェーズを計画することを防ぎます。
- ゼロを含むすべての結果を記録し、測定済みゼロ と 未実行 を区別します。これがラウンドの出力です。何も適用されておらず、それは正しい状態です。
- その上で初めてループに入ります。 スイープが実際に見つけた最も確信度の高い情報源から始めます。
B — すでに進行中のプロジェクト。 キューに入れられたラウンドの前提条件は劣化しますが、その劣化は計画の内部からは見えません。
- まずプロジェクト自身の状態レコードを読みます。 — プログラムバージョン、最後に変更を加えたラウンド、現在期待されているゲートの数など。ノートは証拠ではありません。それらが書かれた時点では真実だったというだけです。
- スコープを決める前に、ラウンドをプログラムと照らして評価する。 読み取り専用の調査1回で、別々のノートが準備完了と記述しているラウンドが誤りであると判明することがよくあります。誤りと判明したラウンドは、安上がりなラウンドです。
- ゲートが現在何を示しているかを確認します。 最後の報告書が述べていた内容ではなく。現在の出力がコミットされた成果物と異なるスイープは、誰も気づかないうちに発生した摂動です。
- そしてループに入ります。 評価プローブが確立した確信度の階層に従います。
どちらの場合も、最初の変更の前に行うこと: 不変条件ブラケットとチェックポイントは、任意の儀式ではありません。それらは Ghidra の静かな副作用に気づく唯一の手段です。プログラムに何かを書き込む前に、以下の「変更の安全性」を読んでください。
実行するラウンドの種類がわかったら、以下の表から対応するカタログに進んでください。
このスキルに含まれるものと、残りの構成要素の場所
SKILL.md は、各ラウンドを拘束するループとルールです。カタログ — 実測されたトラップ、アサーションの規律、ゲーム固有の偵察、API サーフェス — はすべて references/ にあり、必要に応じて読み込まれます。各ファイルには、それを開くべきトリガーが記載されています。
| あなたがこれから… | 読むファイル |
|---|---|
| スイープ、プローブ、ハーベスターを書く、または再実行する — または、それが生成したセンサス、ゼロ、到達可能性の推定値を信じる | references/harvesting-traps.md |
| チェック、ゲート、セルフテスト、ポイズンを書く — または、発動したものを裁定する | references/assertions.md |
| 証拠が自分の作業から独立しているか判断する;型 を読み戻す;階層を信頼する | references/trust-and-circularity.md |
| プログラムを変更する、または復元された型から C ヘッダを出力する | references/applying-changes.md |
| 新しいゲームバイナリを開く:free names、ライブラリとゲームの分離、エンジンと時代 | references/game-recon.md |
| プログラムが影響を与えられないチェックを取得する、または64ビットホストが静かに中継するレイアウトを出力する | references/oracles-and-abi.md |
| クラス、vtable、継承関係、またはテーブル経由のディスパッチを復元する | references/cpp-abi.md |
インポートしていないバイナリ(コンパニオンDLL、デモビルド)のエクスポートを読み取る — このスキル自身の scripts/msvc_demangle を実行する(--pe FILE --tsv) | references/cpp-abi.md |
| スクリプトを書く、または任意の Ghidra API にアクセスする | references/api.md |
| 非 PC または近世以前のターゲットを扱う | references/platforms-eras.md |
| 主張を引用する、または API がバージョンに依存しているか確認する | references/sources.md |
references/api.md は、スクリプトを書く前に読むものです。 後からではありません。Ghidra の API を記憶から呼び出すと、存在しないもっともらしい名前が生成され、最も有用なクラスのいくつかは公開 javadoc に存在しません。また、ラウンドがそもそも構築可能かどうかを決定する2つのこと — MCP 境界(何がツールコールではなくスクリプトを本当に必要とするか)、および 読み取り専用の証人(つまり、PseudoDisassembler、FillOutStructureHelper、バイトパターンエンジンのどの分岐をプログラムに書き込まずに駆動できるか、それぞれ実測されたキャリブレーション値付き)も記載されています。
新しい教訓は、その種類に対応するリファレンスファイルに追加し、ここには追加しないでください。 このファイルは、追記によって3,029行になり、その時点ですべてのラウンドを拘束するルールが、特定の種類のラウンドを拘束するルールの中から見つけられなくなっていました。
ラウンドゼロ — 解析前に free names をスイープする
ゲームは他のターゲットと3つの点で異なり、有用です。開発者向けテキストを異常な量でリークすること、プレイヤーや modder によって書かれた大量の外部ドキュメントが存在すること、そして オンデマンドで状態を変更させることができる ことです。これは静的解析にはない探索プリミティブです。
見返りの高い順に並んでいます。各行は、単一のコマンドでテスト可能な あなたのバイナリに関する仮説 であり、測定されたゼロは発見です。詳細な情報源、ライブラリコードとゲームコードの分離、データ形状、現代のエンジン、プラットフォーム、ランタイム観測については、references/game-recon.md を参照してください。
右側の列は、1つの実例 — あるプロジェクトのフェーズ4時点の、単一の1999年製 MSVC/x86 小売ゲームバイナリ — であり、この手法の特性ではありません。 これは、収量が劇的に異なること、そして ゼロ こそが教訓となるエントリであることを示すためにあります。
| 情報源 | テスト | 実例の収量(1つの1999年 MSVC/x86 小売バイナリ) |
|---|---|---|
| MSVC RTTI | 文字列 .?AV, type_info | 0 — /GR-、そして型付き EH もなし(項目9参照) |
| エクスポートされた vftable シンボル | 文字列 ??_7 | 14 信頼できるクラス→vtable ペア |
| 仮想 / 多重継承 | 文字列 ??_8, ??_9;マングルされたアクセス文字 G/H/O/P/W/X | 0 両方のテストで — 単純な単一継承、2つの独立した方法 |
| マングルされたエクスポートシンボル | ? で始まるシンボルをカウント | 981、クラス、仮想性、完全なシグネチャを含む |
| データエクスポート | .text 外の PE エクスポートディレクトリエントリ | 73 型付き名前付きシンボル — ただし 50 はクラス静的メンバ(@@0/1/2) vs 17 は真のグローバル(@@3) |
| コンパニオンDLLリンケージ | 出荷された DLL のインポートテーブルが exe を参照 | 2つのレンダラープラグイン が46/47の exe シンボルをインポート — レンダラーAPI 語彙全体 |
| 静的初期化子テーブル | .CRT$XC* 境界、null 終端の .text ポインタ配列 | 1429 関数開始点、リンカ契約による |
| ミドルウェア / エンジン | バージョンバナー、既知のインポートセット | Miles Sound System と DirectInput を特定 |
| アサートソースパス | 文字列 .cpp, .c, .h、ドライブレターのプレフィックス | 1 .cpp, 1 .h — ソースルートのみ;リリースビルドはアサートをコンパイル時に除去 |
| デバッグ/PDB 残骸 | .pdb パス、RSDS/NB10、.debug、および exe 横の .MAP/.DBG/.DEF | 測定値 0 — .pdb パス文字列は残存するが、ファイルは出荷されていない |
| ローカライゼーション / 文字列テーブル | 大きな連続した文字列ブロック、id→文字列配列 | 測定値 0 命名情報源として — 存在するが、バイナリ内の何も命名しない |
| 埋め込みスクリプティング VM | 文字列 lua, Lua_、登録テーブルの形状 | 測定値 0 |
| 設定 / CVar パーサー | 既知の設定キーの文字列、.ini, .cfg | 測定値 0 フィールド命名情報源として |
| GUI/プロパティ登録 | クラスごとのレジストラ({name, type, offset} を取る) | 実際に使われているもの — 正確なオフセットでフィールドを命名する15レコード |
ゼロを含むすべての結果を記録し、測定済みゼロ(「検索したが、存在しない」)と 未実行 を区別します。これらを混同すると、プロジェクトは情報源を実際には試していないのに、使い尽くしたと自分自身に思い込ませることになります。そして、上記の4つの行が、このドキュメントのいくつかのリビジョンで、プロジェクトがそれらを測定した後も「未実行」と読まれていたことに注目してください。これは同じ失敗が逆方向に現れたものです。古い「不明」は永遠の機会として読めてしまいます。
ラウンドループ
checkpoint → apply(最も確信度の高い階層のみ) → cascade(スコープ指定)
→ verify invariants → harvest → adjudicate → next round
連続する2ラウンドが新しい成果を生まなくなった時点で停止します。アイデアが尽きた時点ではありません。
各ステージの具体的な意味:
| ステージ | 意味 |
|---|---|
| checkpoint | checkpoint.py スタイル:トランザクション終了、保存、スナップショット、バージョン管理のステップ |
| apply | このラウンドの確信度階層のみを書き込む。SourceType.AI でタグ付けし、apply 引数でゲート制御する |
| cascade | AutoAnalysisManager に何が変更されたかを正確に伝えた後、analyzeChanges を実行する |
| verify invariants | プログラム全体の不変条件を差分比較し、変更があれば raise する |
| harvest | SourceType.AI を除外して、証拠をバージョン管理された証拠ファイルに読み戻す |
| adjudicate | 生の証拠行を 決定された レイアウト/所有権レコードに変換する:確信度ルール(構造的に独立した証拠の種類がいくつ一致するか)を適用し、証人間の競合を解決し、意図的に除外したものとその理由を記録する。これが confidence 列を生成するステップです。除外された証拠は書き留めなければなりません。後日、生の行から再導出する読者は、そうしなければ異なる結論に達し、あなたの答えが間違っていると考えるでしょう。 |
カスケードはスキップされやすいステージ — 強制メカニズムを構築する
このプロジェクトでの実測:このループを認識しているエージェントでも、連続する2回の適用で cascade をスキップしました。 何も失敗せず、警告もなく、ラウンドは完了したように見えました。損害は微妙かつ具体的でした。「デコンパイラは改善したか?」という測定は、再解析が行われていないプログラムに対して行われており、そのため、ある主張された発見は検証されていない読み取りに基づいていました。(後でカスケードを再実行したところ、偶然にもその発見が確認されました。これがまさに、その欠落が見えなかった理由です。)
これは、成果物がない唯一のステージであるため、スキップするのが最も簡単です。apply はシンボルを書き、verify は raise し、harvest は CSV を書き出します。しかし cascade は、後で見て欠落に気づけるようなものを生成しません。そのため、その不在を見えるようにします。
- すべての変更スクリプトは、
AutoAnalysisManagerに何が変更されたかを通知し、analyzeChangesを呼び出して終了する — または、なぜカスケードが適用されないのかを明示的に出力します(例えば、リポジトリCSVのみの適用)。沈黙は、忘れたことと区別がつきません。 - カスケードが実行される前に、適用の効果を測定してはなりません。 「型を適用したが、デコンパイラの出力は同じだ」というのは、アナライザが変更を認識するまで発見とは言えません。
- カスケードを、適用と同じ不変条件でブラケットします。
analyzeChangesは、静かな副作用を引き起こす最も可能性の高い操作であり、再解析は キュレーションを元に戻す 可能性があります。そのため、適用したものがまだ存在することを再アサートします。関数数のみをチェックしても、適用したばかりのすべての構造体をクリアした再解析に気づくことはできません。 - 他のステージと同様にラウンドのメトリクスに記録し、適用はあるがカスケードがないラウンドが、記憶の中だけでなく履歴上で可視化されるようにします。
1ラウンドは1セッションではありません。 ラウンドは作業の単位です。複数のセッションにまたがることができ、長いラウンドは、失敗モードで説明される命名のずれを避けるために、新しいコンテキストに分割する必要があります。ループの終了テスト(「連続する2つの乾いたラウンド」)は作業に関するものであり、エージェントコンテキストがどれだけ長く続くかに関するものではありません。1つのラウンド内では、複数のセッションにまたがる1回の長い実行よりも、短い実行を多数行うことを推奨します。
すべてのラウンドを定量化する。 チェックポイントごとにメトリクス行を追記専用ファイルに出力し、内部一貫性チェック(警告ではなく raise する、合計すべきカウント、互いに素であるべきセット)を設けます。実用的なスキーマの例:label, timestamp, program_version, functions_total、そして追跡している集団ごとに1列(特定されたライブラリ、未分類、適用された型、証拠行)。これにより、各行が完全なスナップショットとなり、任意の2行を比較可能にします。各メトリクスの定義は自身のファイルに文書化し、名前の変更を記録します。定義は変化し、ラウンド間で静かに再定義されたメトリクスは、履歴全体を比較不能にします。これにより、「ラウンドは何かを達成したか」という問いに、印象ではなく答えられるようになります。
リプレイ負債を正直に追跡する。 どのステップがクリーンな状態からまだ再現可能でないのか、そしてその理由を、文書として記録します。エンドツーエンドで誰もリプレイしていないパイプラインは、各ステップが実行時にどれほど正常に見えても、テストされていないのです。
復元された型を C ヘッダとして出力し、コンパイルするオフセットチェックのために _Static_assert でラップすることは、references/applying-changes.md の「C ヘッダの出力」にあります。 — DataTypeWriter を使用すべき理由、CppExporter ではない理由、そして出力された型が固定幅でなければならない理由についても含まれています。
識別結果の適用は、確信度ではなく、ファンアウトによってそのタイミングを決定する。
…[翻訳のために切り詰め]