Binary Analysis 17 · 動的解析 — 実機とエミュレータで動かしながら読む

Chapter 17

動的解析 — 実機とエミュレータで動かしながら読む

この章のゴール.

静的解析(読むだけ)で行き詰まったとき、実機のデバッガかエミュレータで動かして、 メモリ・レジスタ・実行の流れを観察できるようになること。 クラッシュ時のスタックから呼び出し履歴を復元できること。

この章で使う既出の用語(定義は各リンク先). OpenOCD(02 章 7 節)、チェックサム(03 章 8 節)、JTAG(04 章 2 節)、SWD(04 章 2 節)、ELF(05 章 1 節)、スタック(07 章 7 節)、引数(12 章 3 節)、呼び出し(16 章 5 節)、逆アセンブル(16 章 1 節)

1. 静的と動的

動的解析が効くのは、静的では追いきれないとき。「暗号鍵が実行時に計算される」「状態遷移が複雑」「どの経路を通るか入力で変わる」「周辺機器の応答に依存する」場合である。

静的解析と動的解析静的解析(5〜16 章)動かさずにファイルの中身を読む。全体を俯瞰できるが、実行時に決まる値(計算される鍵、通る経路)は追えない動的解析(この章)動かして実行時の状態を観察する。鍵が実行時に計算される・状態遷移が複雑・経路が入力で変わるときに効く実務では両者を往復する(16 章で当たりをつけ、17 章で確かめ、書き戻す)
静的は読むだけ、動的は動かして観察。実行時にしか分からないことに効く

2. 実機デバッガ — gdb + OpenOCD

04 章で吸い出しに使った SWD / JTAG は、そのまま実行制御にも使える。

# 端末 1: OpenOCD をデバッグサーバとして起動
openocd -f interface/stlink.cfg -f target/stm32u5x.cfg
# 端末 2: gdb で接続
arm-none-eabi-gdb firmware.elf         # ELF があればシンボル付き
(gdb) target extended-remote :3333
(gdb) monitor reset halt
(gdb) break main
(gdb) continue
(gdb) info registers                   # R0-R15, PC, SP を見る
(gdb) x/16xw 0x20000000                # RAM を 16 ワード表示
(gdb) dump binary memory ram.bin 0x20000000 0x20040000   # 動作中の RAM をダンプ

できること:

シンボル付き ELF(05 章)があれば関数名・変数名・行番号でデバッグできる。なければアドレスで操作し、16 章の逆アセンブル結果と照らす。

3. エミュレータ — 実機なしで

実機やプローブがない、壊すのが怖い、多数を自動で回したい、というときはエミュレータを使う。

ツール向くもの
QEMU一部の Cortex-M ボード、Linux(Cortex-A)。qemu-system-arm -M mps2-an385 -kernel fw.elf -s -S(-s -S で gdb 待ち受け)
Renode多数の組み込みボードと周辺機器を再現。センサや通信を含む系全体をエミュレート。スクリプトで自動化しやすい
Unicorn EngineCPU コアだけを部分的に実行(関数単位のエミュレーション)。Python から。「この暗号関数だけ動かして出力を見る」

Renode の例:

(monitor) mach create
(monitor) machine LoadPlatformDescription @platforms/boards/stm32f4_discovery.repl
(monitor) sysbus LoadELF @firmware.elf
(monitor) sysbus.cpu LogFunctionNames true    # 実行した関数を記録
(monitor) start

エミュレータの利点は、周辺機器の応答を自由に差し替えられ、実行を記録・再現できることである。難点は、対象ボードや周辺が再現されていないと動かないこと。

実機とエミュレータgdb + OpenOCD / pyOCD(実機)SWD/JTAG で接続。ブレーク・ウォッチ・ステップ・メモリ読み書き。「鍵を使う関数」で止めて RAM を読めば実行時の鍵が見えるQEMU / Renode(系全体)実機なしでボードと周辺を再現。周辺の応答を差し替え、実行を記録・再現できるUnicorn Engine(関数単位)CPU コアだけで特定関数を実行。「この暗号/チェックサム関数だけ動かして出力を見る」エミュレータの利点は周辺応答の差し替えと再現性。難点は対象ボード/周辺の再現が要ること
実機は gdb+OpenOCD、系全体は QEMU/Renode、関数単位は Unicorn

4. 部分エミュレーション — Unicorn

ファーム全体は動かせなくても、特定の関数だけをエミュレートできる。 「この関数はチェックサムを計算しているらしい」という仮説(16 章)を、実際に入力を与えて出力を見て確かめる。

from unicorn import *
from unicorn.arm_const import *
code = open("fw.bin","rb").read()[0x1200:0x1300]   # 対象関数のバイト列
mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
mu.mem_map(0x0, 0x10000)
mu.mem_write(0x1200, code)
mu.reg_write(UC_ARM_REG_R0, 0x20000000)            # 引数を設定
# ... 入力データを RAM に置く ...
mu.emu_start(0x1200 | 1, 0x1300)                   # Thumb は |1
print(hex(mu.reg_read(UC_ARM_REG_R0)))             # 戻り値

これは、鍵導出・チェックサム・独自エンコードなど「アルゴリズムを取り出したい」ときに強力である。

5. クラッシュの解析 — スタックから履歴を復元

機器が HardFault で止まった、ウォッチドッグでリセットした、というとき、止まった瞬間のスタックとレジスタが原因を語る。

Cortex-M は例外発生時、R0-R3・R12・LR・PC・xPSR を自動でスタックに積む。フォールトハンドラや RAM ダンプ(04 章)でこれを取れば:

  1. 積まれた PC = クラッシュした命令のアドレス。addr2line(ELF があれば)でどの行か
  2. 積まれた LR = 呼び出し元。1 つ前の関数
  3. スタックをさかのぼると、戻りアドレス(0x0800xxxx の奇数、07 章)が点在する。これを拾えば呼び出しの履歴(コールスタック)が復元できる
import struct
stack = open("ram_stack.bin","rb").read()
for i in range(0, len(stack), 4):
    v = struct.unpack_from("<I", stack, i)[0]
    if 0x08000000 <= v < 0x08100000 and v & 1:      # フラッシュ範囲の奇数 = 戻りアドレス候補
        print(f"stack+{i:#06x}: {v:#010x} -> {v & ~1:#010x}")
        # arm-none-eabi-addr2line -e fw.elf <v & ~1> で行に

ESP32 の espcoredump.py、Zephyr の coredump、各種フォールトハンドラは、この作業を半自動でやってくれる(12 章)。

クラッシュ解析 — スタックから履歴を復元Cortex-M は例外時に自動退避R0-R3・R12・LR・PC・xPSR をスタックに積む。フォールトハンドラや RAM ダンプ(4 章)でこれを取るPC と LR が原因を語る積まれた PC = クラッシュした命令。LR = 呼び出し元。addr2line(ELF があれば)で行にスタックをさかのぼるlocal変数0x080012a5戻りアドレス引数0x08003f11戻りアドレス…フラッシュ範囲の奇数値(0x0800xxxx | 1)を拾えば、呼び出しの履歴(コールスタック)が復元できるESP32 espcoredump.py、Zephyr coredump が半自動化(12 章)
例外時に PC/LR がスタックに積まれる。フラッシュ範囲の奇数値を拾えばコールスタックが復元できる

6. 通信を覗く

機器が外とやり取りするなら、通信の観察も動的解析である。

これらで得た通信フレームは、また 03 章の構造解析の対象になる。

7. 静的と動的を往復する

実務では両者を行き来する。

  1. 静的(16 章)で「この関数が鍵を作るらしい」と当たりをつける
  2. 動的でその関数にブレークを置き、実行時の入出力を見て確かめる
  3. 分かったことを静的解析に書き戻す(関数名・型を付ける)
  4. 次の疑問へ
静的と動的の往復静的で当たりこの関数が鍵を作るらしい(16 章)動的で確認ブレークして実行時の入出力を見る書き戻す関数名・型を付ける次の疑問へ解析を育てる分かったことを静的解析に書き戻し、次へ。解析は 1 回で終わらず育てる作業
静的で当たりをつけ、動的で確かめ、書き戻す。解析は育てる作業

8. 手を動かす

動的解析の手段を選ぶ


この章のポイント