Chapter 16
逆アセンブルの基礎 — 機械語から関数を読み解く
この章のゴール.
Ghidra に生バイナリを正しく読み込ませ(アーキテクチャ・ロードアドレス)、 関数・文字列・呼び出し関係をたどって「このコードが何をするか」の当たりをつけられるようになること。 シンボルがないコードで、周辺レジスタや定数から機能を推定する手がかりを持つこと。
この章で使う既出の用語(定義は各リンク先). Ghidra(02 章 6 節)、CRC(03 章 8 節)、オフセット(03 章 1 節)、チェックサム(03 章 8 節)、マジックナンバー(03 章 6 節)、リトルエンディアン(03 章 2 節)、ELF(05 章 1 節)、ベクタテーブル(07 章 1 節)、リセットハンドラ(07 章 2 節)、リテラルプール(07 章 6 節)、ロードアドレス(07 章 10 節)、周辺レジスタ(07 章 7 節)、定数やアドレス(07 章 6 節)、MCUboot(08 章 1 節)、エントリ(11 章 2 節)、読み方(11 章 2 節)、バイナリ(12 章 3 節)
1. 逆アセンブルと逆コンパイル
- 逆アセンブル(disassemble): 機械語(バイト列)をアセンブリ命令に戻す。1 対 1 に近い
- 逆コンパイル(decompile): さらに C 風の擬似コードに戻す。読みやすいが推測が入る
Ghidra は両方を並べて見せる。まず擬似コードでざっと把握し、疑問があればアセンブリで確かめる、という使い方が基本である。
2. Ghidra に読み込ませる — 3 つの設定
生バイナリ(.bin)を開くときは、Ghidra が構造を知らないので、3 つを人間が指定する。ここを間違えると全部ずれる。
設定 1: アーキテクチャ(Language)
- Cortex-M:
ARM:LE:32:Cortex(リトルエンディアン、Thumb) - Cortex-A(Linux):
ARM:LE:32:v7やAArch64:LE:64:v8A - ESP32: Xtensa(Ghidra 本体は非対応。プラグインが要る)または RISC-V
ELF なら Ghidra が自動判定する(05 章)。生バイナリは 07 章で調べた CPU を指定する。
設定 2: ロードアドレス(Base Address)
生バイナリのオフセット 0 が、実機のどのアドレスに焼かれるか(07 章で推定した値)。 Cortex-M アプリなら 0x08000000 や 0x08008000(MCUboot の後ろ、08 章)。 これが正しいと、コード中のポインタ(リテラルプール)が正しい場所を指し、文字列や関数への参照がつながる。 間違っていると参照が全部外れる。
設定 3: エントリポイント / ベクタテーブル
Cortex-M なら、先頭をベクタテーブルとして解釈させ、リセットハンドラ(オフセット 4、Thumb ビットを落とす)を関数として解析開始点にする。 Ghidra の「ARM Cortex」ローダや、手動で Disassemble(Thumb モード)を指定する。
3. 自動解析が見つけるもの
読み込むと Ghidra が自動解析(Analyze)を走らせ、次を見つける。
- 関数:
push {lr}/pop {pc}などの入口・出口から関数境界を推定 - 文字列: 印字可能バイトの連続を文字列としてマーク
- 相互参照(XREF): 「このアドレスをどこから参照しているか」。文字列やレジスタから関数の役割を逆にたどれる
- 呼び出しグラフ: 関数間の呼び出し関係
4. どこから読むか — とっかかりの見つけ方
シンボルがないと、数百の FUN_08001234 が並ぶだけである。意味のある場所から入る。
- 文字列から:
stringsで見つけた特徴的な文字列("Login failed"、"/dev/ttyS0"、フォーマット文字列)を Ghidra で探し、その文字列を使っている関数を XREF でたどる。エラーメッセージは機能の目印 - 周辺レジスタから: コード中の
0x40000000台の定数は、ハードウェアレジスタのアドレス。データシートでレジスタを引けば、「この関数は UART / SPI / GPIO / フラッシュを触っている」と分かる - 既知の定数から: 暗号の定数(AES の S-box、SHA の初期値
0x6a09e667、CRC 多項式)、マジックナンバー(08 章・10 章)を検索すると、暗号・チェックサム・特定形式を扱う関数が見つかる - 割り込みハンドラから: ベクタテーブル(07 章)の各エントリが、その周辺機器の割り込み処理。SysTick ハンドラはタイマ、USART ハンドラは通信
- エントリポイントから: リセットハンドラ → スタートアップ →
mainと順にたどる
5. Cortex-M の命令を読む勘どころ
擬似コードで足りることも多いが、アセンブリを読むとき知っておくこと。
- 関数の入口:
push {r4-r7, lr}。出口:pop {r4-r7, pc}またはbx lr - 呼び出し:
bl 関数(戻りアドレスを LR に)、blx r0(レジスタ経由) - 引数: R0〜R3 が第 1〜4 引数、戻り値は R0(Arm の呼び出し規約 AAPCS)
- 定数の読み込み:
ldr r0, [pc, #off]で近くのリテラルプール(07 章)から 32 ビット値。Ghidra は解決して定数やアドレスを表示する - 周辺アクセス:
ldr/strで 0x40000000 台のアドレスを読み書き = レジスタ操作 - Thumb-2: 一部命令は 4 バイト。Ghidra が誤って 2 バイトずつ読むとズレる。ズレたら手動で再解析
6. 逆コンパイラの読み方と限界
Ghidra の逆コンパイラは C 風コードを出すが、推測が入ることを忘れない。
- 型は推定。
undefined4、uintは「4 バイトの何か」。手動で型を付けると読みやすくなる - 最適化されたコード(インライン展開、ループ変形)は、元のソースと形が違う
- 変数名は自動(
local_10、uVar3)。意味が分かったら名前を付けて進める(解析は育てる作業) - ポインタ演算とビット操作が多いコード(ドライバ、パーサ)は擬似コードが読みにくい。アセンブリと往復する
7. objdump で足りることも
ELF があってシンボルも残っているなら(05 章)、Ghidra を開くより速い。
arm-none-eabi-objdump -d -j .text firmware.elf | less
arm-none-eabi-objdump -d firmware.elf | grep -A30 "<uart_send>:" # 関数名で絞る「Ghidra は生バイナリや大規模解析のため、objdump はシンボル付き ELF の一関数を素早く見るため」と使い分ける。
8. 手を動かす
逆アセンブルのとっかかりを選ぶ
この章のポイント
- 生バイナリを Ghidra に読ませるには アーキテクチャ・ロードアドレス・ベクタテーブルの 3 設定。ロードアドレスが正しいと参照がつながる
- 意味のある入口は 文字列・周辺レジスタ(0x4000 台)・既知の定数・割り込みハンドラ。XREF で役割を逆にたどる
- Cortex-M は R0-R3 が引数、
blが呼び出し、リテラルプールが定数。Thumb のズレに注意 - 逆コンパイラは便利だが推測。型と名前を付けながら育てる
- シンボル付き ELF なら objdump -d が速い