Chapter 19
自動化とパーサ — 使い捨てず、再現できる形にする
この章のゴール.
手で解いた構造を、Python の struct・construct・Kaitai Struct で再利用できるパーサにできるようになること。 解析を「1 回きりの手作業」ではなく「同じ入力から同じ結果が出る、検証済みの手順」に変えること。
この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、Ghidra(02 章 6 節)、binwalk(02 章 3 節)、unblob(02 章 3 節)、CRC(03 章 8 節)、オフセット(03 章 1 節)、固定長(03 章 5 節)、TLV(08 章 4 節)、バージョン(08 章 3 節)、逆アセンブル(16 章 1 節)
1. なぜ自動化するか
手で hexdump を読んで構造を解くのは、最初の 1 回には必要である。だがそこで終わると、次の 3 つで困る。
- 再現できない: 半年後に同じダンプを見て、どう解いたか思い出せない
- 横展開できない: 同じ形式の別のダンプ(別ロット、別バージョン)に、また手作業
- 検証できない: 「この解釈で本当に全レコードが読めるか」を確かめられない
構造が分かった時点でパーサに落とすと、これらが解決し、差分(18 章)や監査(13 章)を仕組み化できる。
2. まず struct — 固定構造
03 章・08 章で使った struct は、固定長・アラインメントの分かった構造に最適である。
import struct
from dataclasses import dataclass
@dataclass
class McubootHeader:
magic: int; load: int; hdr_size: int; ptlv: int; img_size: int
flags: int; v_major: int; v_minor: int; v_rev: int; v_build: int
@classmethod
def parse(cls, data, off=0):
f = struct.unpack_from("<IIHHII BBHI", data, off)
h = cls(*f)
assert h.magic == 0x96f3b83d, f"bad magic {h.magic:#x}"
return h
d = open("slot0.bin","rb").read()
h = McubootHeader.parse(d)
print(h)assert でマジックや長さの妥当性を検査すると、構造の理解違いやオフセットずれがすぐ露見する。
3. construct — 可変長・条件分岐
繰り返し・可変長・条件で形が変わる構造(10 章〜12 章の追記型ストレージ、TLV、ログ)には、宣言的に書ける construct ライブラリが向く。
from construct import Struct, Const, Int32ul, Int16ul, Int8ul, Bytes, this, GreedyRange
TlvEntry = Struct(
"type" / Int16ul,
"length" / Int16ul,
"value" / Bytes(this.length),
)
Trailer = Struct(
"magic" / Const(b"\x07\x69"), # 0x6907
"total" / Int16ul,
"entries" / GreedyRange(TlvEntry),
)
t = Trailer.parse(trailer_bytes)
for e in t.entries:
print(hex(e.type), e.length, e.value.hex())construct は「読む」だけでなく「書く」(build)もできるので、テストデータの生成(既知の構造を作って、パーサが正しく読めるか確かめる)にも使える。
4. Kaitai Struct — 言語非依存 + 可視化
Kaitai Struct は、構造を .ksy(YAML)で 1 回書くと、Python・C++・Java・JS などのパーサを自動生成し、さらに Web IDE でバイト列に色を付けた可視化が得られる。チームで共有する形式仕様に向く。
# mcuboot_header.ksy
meta:
id: mcuboot_header
endian: le
seq:
- id: magic
contents: [0x3d, 0xb8, 0xf3, 0x96]
- id: load_addr
type: u4
- id: hdr_size
type: u2
- id: protect_tlv_size
type: u2
- id: img_size
type: u4
- id: flags
type: u4
- id: version
type: version_t
types:
version_t:
seq:
- id: major
type: u1
- id: minor
type: u1
- id: revision
type: u2
- id: build
type: u4kaitai-struct-compiler -t python mcuboot_header.ksy # Python パーサを生成
# ide.kaitai.io に .ksy とダンプを読ませると、色付きの構造ビューが見られるKaitai Web IDE の可視化は、構造の検証と教育(他人に「ここがヘッダ」と示す)に非常に有用である。
5. どれを使うか
| 状況 | 道具 |
|---|---|
| 固定長、その場で 1 回 | struct(1 行で済む) |
| 可変長・繰り返し・条件、Python で完結 | construct |
| 多言語で共有、可視化したい、形式仕様として残す | Kaitai Struct |
| 逆アセンブルの自動化 | Ghidra のスクリプト(Python / Java)、angr |
| 既知形式の一括抽出 | binwalk / unblob をスクリプトから |
6. 解析パイプラインを組む
個々のパーサができたら、ダンプ → 分類 → 各領域のパース → 報告を 1 本のスクリプトにする。
def analyze(path):
d = open(path,"rb").read()
report = {"sha256": sha256(d), "size": len(d)}
report["map"] = classify_by_entropy(d) # [14 章](14_圧縮と暗号化の見分け.md#エントロピーで領域を塗り分ける)
for region in report["map"]:
if region.kind == "mcuboot":
report[region.name] = McubootHeader.parse(d, region.off) # [08 章](08_ブートローダとイメージ形式.md#イメージヘッダ-先頭-32-バイト)
elif region.kind == "nvm3":
report[region.name] = parse_nvm3(d, region.off, region.size) # [10 章](10_NVM3.md#nvm3-の構造)
elif region.kind == "secrets":
report["secrets"] = scan_secrets(d, region) # [13 章](13_秘密情報の捜索.md)
return reportこれを CI に載せれば、ビルドのたびに「秘密が平文で残っていないか」「バージョンが正しいか」を自動チェックできる(13 章)。
7. 検証を組み込む
自動パーサには、「正しく読めているか」を確かめる仕掛けを必ず入れる。
- マジックと固定値の assert(構造の入口)
- CRC / ハッシュの照合(03 章・18 章)
- 全体を読み切れたか: パースが領域の末尾までちょうど到達するか。途中で余る / 足りないなら構造の理解違い
- 既知の値との突き合わせ: 分かっている設定値(シリアル番号など)が、パース結果に現れるか
- ラウンドトリップ: construct や Kaitai で読んだものを書き戻すと、元のバイト列に一致するか
8. 記録する
パーサとスクリプトは、ダンプの由来(01 章)・メモリマップ・分かったこと・分からなかったことと一緒に、リポジトリに残す。 「未来の自分(と同僚)が、同じダンプから同じ結論に、より速く到達できる」状態にするのが、解析の完成である。
9. 手を動かす
構造から道具を選ぶ
この章のポイント
- 構造が分かったらパーサに落とす。再現・横展開・検証ができるようになる
- struct(固定長)→ construct(可変長・条件)→ Kaitai Struct(多言語 + 可視化 + 形式仕様)と使い分け
- ダンプ → 分類 → パース → 報告をパイプライン化し、CI で秘密・バージョンを自動チェック(13 章)
- パーサには検証(マジックの assert、CRC 照合、末尾まで読み切る、ラウンドトリップ)を必ず入れる
- パーサ・スクリプト・メモリマップ・由来を残すのが解析の完成