Binary Analysis 19 · 自動化とパーサ — 使い捨てず、再現できる形にする

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 章)を仕組み化できる。

なぜパーサに落とすか再現できる半年後に同じダンプを見て、どう解いたか思い出せる横展開できる同じ形式の別ダンプ(別ロット・別版)に、また手作業しなくて済む検証できる「この解釈で全レコード読めるか」を確かめられる。差分・監査を仕組み化手で解くのは最初の 1 回。構造が分かった時点でパーサに落とす
再現・横展開・検証のためにパーサに落とす。手作業は最初の 1 回だけ

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: u4
kaitai-struct-compiler -t python mcuboot_header.ksy   # Python パーサを生成
# ide.kaitai.io に .ksy とダンプを読ませると、色付きの構造ビューが見られる

Kaitai Web IDE の可視化は、構造の検証と教育(他人に「ここがヘッダ」と示す)に非常に有用である。

3 つの道具の使い分けstruct固定長・アラインメントの分かった構造。1 行で済む。assert でマジック・長さを検査construct可変長・繰り返し・条件で形が変わる構造(TLV・追記型ストレージ)。宣言的に書ける。build でテストデータ生成もKaitai Struct.ksy を 1 回書くと多言語パーサを生成 + Web IDE で色付き可視化。チームで共有する形式仕様に固定 → struct、可変 → construct、多言語+可視化+仕様 → Kaitaiダンプ → 分類 → 各領域パース → 報告 をパイプライン化し、CI で秘密・バージョンを自動チェック(13 章)
固定は struct、可変は construct、多言語・可視化・仕様は Kaitai

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 / ハッシュの照合3・18 章。合えば解釈が正しく壊れていない末尾まで読み切れるかちょうど到達すれば OK。余る/足りないなら理解違いラウンドトリップ読んだものを書き戻すと元のバイト列に一致するかパーサ・スクリプト・メモリマップ・由来を残すのが解析の完成
assert・CRC 照合・末尾まで読み切る・ラウンドトリップで「正しく読めているか」を確かめる

8. 記録する

パーサとスクリプトは、ダンプの由来(01 章)・メモリマップ・分かったこと・分からなかったことと一緒に、リポジトリに残す。 「未来の自分(と同僚)が、同じダンプから同じ結論に、より速く到達できる」状態にするのが、解析の完成である。

9. 手を動かす

構造から道具を選ぶ


この章のポイント