HN公開: プログラムの一部をオブジェクトファイルとしてエクスポートする Ghidra 拡張
(github.com/boricj)- この Ghidra 拡張は、プログラムの一部を オブジェクトファイル としてエクスポートでき、エクスポートされたファイルにはシンボルや再配置テーブルなどの有効なメタデータが含まれるため、ツールチェーンで再処理できる
- 主な用途は 高度なバイナリパッチ, ソフトウェア移植, ファイル形式変換, ライブラリ作成, デコンパイルプロジェクトでプログラムを複数のオブジェクトファイルに分割して再実装する作業
- サポートされる組み合わせは COFF x86/x86_64, ELF x86/x86_64/MIPS, OMF x86 であり、COFF MIPS・OMF x86_64・OMF MIPS はサポートされない
- ユーザーは Ghidra Listing で抽出するアドレス範囲を選択し、Relocation table synthesizer アナライザーを実行した後、
File > Export Program…で再配置可能オブジェクトファイル exporter を呼び出す - 再配置テーブルの合成は Ghidra データベースの正確性 に依存するため、誤った情報や欠落した情報があると再配置が壊れたり欠落したりする可能性があり、エクスポート直前にアナライザーを再実行するのが安全
これはどんな拡張か
- Object file exporter extension for Ghidra は、プログラムの一部をオブジェクトファイルとしてエクスポートできるようにする Ghidra 拡張
- 生成されたオブジェクトファイルには シンボル、再配置テーブル などの有効なメタデータが含まれる
- このメタデータにより、エクスポートしたオブジェクトファイルをツールチェーンで直接再利用して追加処理できる
活用例
- Advanced binary patching
- 元の部分と修正後の部分を手作業で合わせる代わりに、リンカー を使って一緒に整合させられる
- Software ports
- プログラムからシステム非依存コードを分離し、残りを置き換えられる
- プログラム やオブジェクトファイルを、あるファイル形式から別のファイル形式へ変換できる
- プログラムの一部抽出 と ライブラリ作成
- プログラムの一部を抽出して別の文脈で再利用できる
- デコンパイルプロジェクトでは、プログラムを複数のオブジェクトファイルに分割し、それを Ship of Theseus 的な方法で再実装できる
サポートされるアーキテクチャとオブジェクトファイル形式
- サポートマトリクスは次のとおり
- COFF: x86, x86_64 をサポート / MIPS は非対応
- ELF: x86, x86_64, MIPS をサポート
- OMF: x86 をサポート / x86_64, MIPS は非対応
ビルドとインストール
- CLI ビルド手順
- リポジトリを clone する
GHIDRA_INSTALL_DIR環境変数を Ghidra のインストールディレクトリに設定するgradle buildExtensionを実行する- 生成された Ghidra 拡張アーカイブは
dist/ディレクトリに作成される
- GitHub Maven リポジトリからパッケージをダウンロードするには 認証 が必要
read:packages権限を持つ GitHub classic token を作成し、${GRADLE_USER_HOME}/gradle.propertiesにgithubToken=ghp_xxxを追加する- または
gradle installStandaloneDepsを実行して、submodule の vendored dependency をビルドしてインストールする
- インストール手順
- releases page から拡張をダウンロードするか、ローカルでビルドする
- Ghidra で File > Install Extensions… から拡張をインストールする
- CodeBrowser ウィンドウの File > Configure > Experimental で RelocationTableSynthesizedPlugin プラグインを有効にする
使用フローと注意点
- 基本的な使用手順
- Listing ビューで抽出する アドレス集合 を選択する
- one-shot mode で提供される Relocation table synthesizer アナライザーを実行する
- File > Export Program… で再配置可能オブジェクトファイル exporter を呼び出す
- 再構成された再配置は Window > Relocation table (synthesized) で確認できる
- 詳細な評価レポートは Analysis > Auto Analyze... ダイアログで Relocation table synthesizer アナライザーの Evaluation report policy オプションを設定して有効化できる
- この拡張を使う前に、プログラム全体をあらかじめすべてリバースエンジニアリングしておく必要はない
- delinking の成功は一般に、エクスポートする部分と外部参照内のメタデータに大きく依存する
- 再配置位置として使われる関数とポインタ
- 再配置対象として使われるシンボル footprint
- その両者の間の参照
- Relocation table synthesizer アナライザーは Ghidra データベースの正確性に依存する
- 不正確または欠落した情報は、解析中に壊れた再配置や欠落した再配置につながる可能性がある
- オブジェクトファイル exporter は Relocation table synthesizer の解析結果に依存する
- 疑わしい場合は、オブジェクトファイルをエクスポートする直前にアナライザーを実行して、再配置テーブルが最新か確認すべき
仕組み
- オブジェクトファイルは 3 つの部分で構成される
- 再配置可能なセクションバイト
-
シンボルテーブル
- 再配置テーブル
- リンカーが複数のオブジェクトファイルから実行ファイルを作るときに行う処理
- セクションをメモリに配置する
- 仮想アドレス空間でシンボルアドレスを計算する
- 最終的なシンボルアドレスに基づいてセクションバイトへ再配置を適用する
- 通常、この処理が終わると 再配置テーブル は破棄される
- デバッグシンボルが保持されなければ シンボルテーブル も破棄され、再配置できないセクションバイトだけが残る
- この拡張は慎重な解析によってそのデータを再構築でき、これによりプログラムを再びオブジェクトファイルへ delink できる
1件のコメント
Hacker Newsのコメント
ここで見かけてうれしい。本当にすばらしいプロジェクトだと思っていて、MS COFF サポートの追加を手伝った
ただ、私の最初の PR は既存の ELF サポートよりかなり出来が悪かったので、もし問題が起きたらたぶん私のせいかもしれない。それでも改善されているのは見えている
まだ大きな作業には使っていないが、Visual Studio 2003 でコンパイルした Hello World 実行ファイルを delink して、Linux x86 の GCC+glibc で再リンクし、さらに MinGW+msvcrt で再リンクしてみたのがいちばん面白かった
Hello World より大きいものはまだ難しく、とくに Ghidra にあまり詳しくないので、大きなバイナリで delink する範囲を選ぶよい方法もまだ見つけられていない
ちょうど今日、このツールの Nixpkgs 派生パッケージがマージされて、NixOS unstable では
ghidra.withExtensionsでインストール可能になった。場所はghidra-extensions.ghidra-delinker-extensionただし数日前に新バージョンが出たのに PR をリベースしていないので、今は旧バージョンで、近いうちに更新を出す予定
たとえば、元の実行ファイルを構成していた複数のオブジェクトファイルの名前と範囲を把握した Ghidra プログラムがあるなら、そのフォルダやフラグメントを右クリックして > Select Addresses で丸ごと選択できる
再配置合成アナライザとエクスポートもそれぞれスクリプト化したり、プログラムのツリー管理機能を通じて自動化したりできるので、必要な範囲を手で選んでアナライザとエクスポートを手動実行する必要が減る
かなり興味深そうで、数年前に中断したゲームのリバースエンジニアリングプロジェクトをまた見直したくなった
これをどう使って、その出力をどう活用するのかを最後まで見せる完全な例があるといい
https://github.com/boricj/ghidra-delinker-extension/blob/mas...
実行ファイルのどのセクションをエクスポートするか見極めるのに、どれくらい手間がかかるのか気になる
2008〜2015 年ごろの比較的モダンなWin32 ゲームをオブジェクトファイルとしてエクスポートしたあと、数時間以内に再び完全な実行ファイルとしてコンパイル/リンクするのは現実的だろうか?
何をエクスポートするかは別問題で、プログラムに関する知識が必要だ。デバッグシンボルがあればずっと簡単になるし、なくてもエクスポートできる程度に Ghidra データベースを正確に作り込めば、たいてい何がどこにあるかの感覚はつかめる
投稿本文のユーザー事例では、7 月初めに最初の issue を立てて、8 月中旬ごろには機能的に同等に動く再リンク済み実行ファイルを得ていた
ただし当時は COFF エクスポートに修正すべきバグが多く、i386 アナライザにも手を入れるべき点があったので、今では他の人が同じ問題にあまりぶつからないことを期待している
どれくらい時間がかかるかは分からないが、デバッグシンボルがあって本当に運がいい場合でない限り、数時間よりは長くかかる可能性が高い。熟練したリバースエンジニアならその時間内に何かしら動くところまでは持っていけるかもしれないが、最初のロード画面の途中で落ちるかもしれず、終わるまでいつ終わるか分からない作業に近い
すごく良さそうだ。いつか既存プログラムをもっと簡単に分解して、望む形で使う助けになるかもしれない
Meta の LLM Compiler や LLM を使ったデコンパイルのような研究があるが、プログラムを分割して一部を置き換える作業は、LLM が探索して自ら改善していくうえでも、データが豊富な興味深い領域かもしれない
学習の観点でも興味深いトークンがたくさん潜んでいて、それでできることも多そうだ
正直、魔法のように聞こえる。どうやって可能なのか理解してみたい
リンカが複数のオブジェクトファイルから実行ファイルを作るときは、セクションをメモリに配置し、仮想アドレス空間でシンボルのアドレスを計算し、その最終的なシンボルアドレスに基づいてセクションのバイト列へ再配置を適用する。
delinkの要点は、その再配置がどこに適用されたかを見つけて元に戻し、再び再配置可能なバイト列を得ることにある。そのあと、戻した内容をもとに再配置テーブルとシンボルテーブルを作ってまとめれば、オブジェクトファイルになる。
本当に難しい部分は、再配置箇所を見つけ出す解析だ。大半はGhidraに任せられるが、参照を再配置箇所へ変換する処理が必要になる。x86では比較的簡単だが、MIPSでは悪夢のように難しい。必要なデータを集めてオブジェクトファイルをシリアライズする作業まで自動化するために、この拡張を作った
Microsoftプラットフォームを除けば、実際のファイル形式が同じことも多く、たとえばELFがそうだ。
大きな違いは再配置にある。オブジェクトファイルには細かい再配置情報があるが、実行ファイルには普通はない。Windowsの実行イメージには、実行ファイルが再配置されるときに修正すべきコードやデータのアドレスを示す最小限の再配置しかなく、イメージ自体はイメージ基準アドレス全体単位でしか再配置できない。
一方でオブジェクトファイルには シンボル単位の再配置 が入っている。この情報を正確に再構成するには、逆アセンブル結果にかなり正確なシンボル情報を結び付ける必要がある。
もう1つの大きな違いは、オブジェクトファイルがまだリンクされていないことだ。どのシンボルも未解決の状態にある。これはむしろ修正しやすく、delink中に現在の範囲外にあるシンボルなら、たいてい未解決シンボルにすればよい。その後の再リンク時に、ほかのオブジェクトファイルやライブラリがそのシンボルを提供すれば再びつながる。
エントリポイントがないといった小さな違いもあるが、大きな意味はない。
したがって、実行イメージや共有オブジェクトからオブジェクトファイルを切り出す境界は、実質的には任意だ。元のコンパイル時には翻訳単位ごとに分かれていたはずだが、リンク段階ではその境界はあまり意識されない。もちろん、一致するデコンパイルを望むなら、オブジェクトファイル境界を間違えると非常に難しくなりうるので、可能なら把握したほうがよい。
実際にこの問題を扱ったことはあるが、細部を少し間違えているかもしれないので参考程度に見てほしい。オブジェクトファイルについてブログ記事を書こうと思っていたし、すでによい記事もいくつかある
この過程が完全に安全なのか気になる。常に成功が保証されるのか、それとも解析が保守的に動作するのか知りたい。
たとえばELFに何らかの断片、データ、機能が欠けているとdelinkが失敗する、という感じなのだろうか?
私の解析器は、少なくともエクスポートしようとしている部分については 正確なGhidraデータベース に依存している。修正すべきさまざまな問題をログに残そうとかなり努力はしているが、存在しないものは見えない。
特に、欠落した参照や切り詰められた変数は検出されず、奇妙な未定義動作につながることがある。
こうした問題の一部を追跡する方法はある。これまで見つけた最善の方法は、実行ファイルを別の基準アドレスで再リンクし、元のプログラムのアドレス範囲をマッピングしないようにすることだ。すると見落とした絶対再配置箇所でセグメンテーションフォルトが起き、デバッグできる。ただし、対象にMMUがある場合に限られる。
切り詰められた変数は、特に疑っていないと追跡が非常に難しい。壊れるのは切り詰められた変数の後ろにあるメモリだからだ。整数をポインタと取り違えた場合も追跡しにくい。整数値が対象シンボルの配置アドレスによって変わり、プログラムの動作が不安定になるためで、特にアドレス空間のかなり低い位置にロードされるプログラムで問題が大きい。
それでも、Ghidraデータベースが十分に正確で、元と同じオブジェクトファイル形式で再エクスポートし、同じプラットフォームと同じツールチェーンを使うなら、メガバイト単位のプログラムコードとデータをうまくdelinkできる。リンカがやったことなら、逆にたどれるはずだと考えている。
逆に、Linux i386 ELF実行ファイルからCOFFオブジェクトファイルへdelinkしてi386 Windowsツールチェーンで使う、といった具合に、元のプログラムのプラットフォームやツールチェーンに合わない クロスdelink を始めると話は変わってくる。エクスポート側が再配置を表現できるなら、動作する再配置可能オブジェクトファイルを得られるかもしれないが、ABIの不一致とも戦わなければならない。可能ではあるが、最初のプロジェクトとしては勧めない。
要するに、何をするかとGhidraデータベースの正確さ次第で、「そのまま動く」から「クトゥルフに慈悲を祈る」まで幅がある
たとえば
int getSpecialArrayElement(char *array, uint64 key) { i = computeOffset(key); return array[i]; }のような関数を考えればよい。computeOffsetはいくらでも複雑にできるし、難読化したいなら意図的に解析しづらくすることもできる。この関数がメモリの任意の位置へアクセスするのを防ぐものはない。可能なすべての入力を試さない限り、リンカがすでにメモリ上に定義しているシンボルへアクセスするかどうかは分からないし、
computeOffsetにはその試行を妨げる チューリングトラップ を組み込むこともできる以前は想像するだけで実際には作らなかったアイデアと結びつけると、面白そう。デバッグ情報からヘッダーファイルを生成し、必要ならLLMに整理させるというもの
https://github.com/wbenny/pdbex
LLMで整理する部分については、まだリバースエンジニアリングにLLMモデルを適用して大きな成功を収めた事例はあまり多くないように見える。もしかすると、この領域はLLMアーキテクチャの限界が現れる場所なのかもしれないとも感じる
専門家ではないが、賭けるなら多くのリバースエンジニアリング用途では拡散モデルのほうがより興味深い気がする
まったく同じではないが、Binary NinjaにはSidekickという機能があり、LLMで逆アセンブル結果を整理しようとしている。個人的にはあまり印象的ではなかったが、誰かにとっては有用かもしれない
paholeはELF DWARF情報からコンパイル可能なCヘッダーファイルを作ってくれるここではLLMはあまり関係なさそうに見える。ヘッダーファイルが実行ファイルからエクスポートされたすべての型を元の値とともに正しく含んでいて使えるか、あるいは間違っているか不完全かのどちらかだ。LLMに何かをさらに作り出させても助けにはならない
Ghidraにもデータ構造をエクスポートする標準機能があり、DWARF構造から生成できる。右クリック -> Export to C header
ヘッダー生成と、その後に型制約に合わせて修正できるミューテーター生成もその一部だった
LLM要素を付け加えるなら、無名構造体のようなものに名前を付ける程度はできそうだが、それが良い考えかは分からない。より興味深いのは、文書化目的で既知の型制約をLLMに自然言語で要約させる試みかもしれない
まだ実装していない理由は、これまではなくても何とかやってこられたから。しかもかなり深い沼のように聞こえるし、今入っている沼もすでに十分大きい
本当にすごく良さそうで、以前考えていたゲーム改造のアイデアにも関係がある。Tenchuデコンパイルのブログシリーズも良かった
今やっている仕事ではすぐに使い道はないが、以前やっていたなら本当に役立ったはずのツールに見える
近いうちに試す時間か機会ができるといい