Binary Analysis 18 · 差分と壊れたデータ — 2 つを比べ、破損を見抜く

Chapter 18

差分と壊れたデータ — 2 つを比べ、破損を見抜く

この章のゴール.

2 つのダンプ(更新前後・正常と異常・機器間)を比べて変わった場所だけを取り出せるようになること。 電源断やフラッシュ劣化で壊れたダンプを、破損か自分の解釈違いかを切り分けて扱えること。

この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、CRC(03 章 8 節)、アラインメント(03 章 4 節)、エンディアン(03 章 2 節)、オフセット(03 章 1 節)、ロードアドレス(07 章 10 節)、MCUboot(08 章 1 節)、バージョン(08 章 3 節)、LittleFS(09 章 2 節)、メタデータペア(09 章 4 節)、NVM3(10 章 1 節)、NVS(11 章 1 節)、読み方(11 章 2 節)、タイムスタンプ(12 章 3 節)、U-Boot(15 章 1 節)

1. なぜ差分が強力か

「ゼロから全部を理解する」のは大変だが、「2 つを比べて違いだけを見る」のは桁違いに速い。

なぜ差分が強力か更新前後書き換わった場所 = 更新が触った領域。バージョン・設定・コードのどこが変わったか正常機と異常機違う場所に異常の原因がある可能性が高い操作の前後ある操作がフラッシュのどこに何を書いたか = 未知の構造を解く最短路同一ロットの複数機共通=固定値/コード、違う=個体データ(シリアル・鍵・較正値)ゼロから理解するより、2 つを比べて違いだけ見るのは桁違いに速い
更新前後・正常異常・操作前後・個体間を比べ、変わった場所だけ見る

2. バイト単位の差分

cmp -l a.bin b.bin | wc -l               # 違うバイト数
cmp -l a.bin b.bin | head                # 位置(10 進、1 始まり)と値(8 進)
radiff2 a.bin b.bin                       # 16 進で見やすい差分
radiff2 -s a.bin b.bin                    # 類似度(%)
vbindiff a.bin b.bin                      # 並べて対話的に

サイズが違う / ずれている場合、単純なバイト比較は「1 か所ずれると以降全部違う」と出て使えない。 その場合はブロック単位・構造単位で比べるか、radiff2 のアラインメント機能、bsdiff の差分を使う。

3. 差分を意味に変える

違うバイトの一覧が出たら、それを 01 章のメモリマップに重ねる。

a = open("before.bin","rb").read()
b = open("after.bin","rb").read()
runs, start = [], None
for i in range(min(len(a),len(b))):
    if a[i] != b[i]:
        if start is None: start = i
    else:
        if start is not None: runs.append((start, i)); start = None
if start is not None: runs.append((start, len(a)))
for s,e in runs:
    print(f"{s:#08x}-{e:#08x} ({e-s} B)  {a[s:e][:8].hex()} -> {b[s:e][:8].hex()}")

変化した区間が、どの領域(コード・設定・ログ・バージョン)かを見て、意味を推定する。

差分を意味に変える小さな連続した変化カウンタ・バージョン・タイムスタンプ4KB / 64KB 境界に揃った大きな変化フラッシュブロックの書き換え(設定更新・ログ追記)コード領域の変化ファーム更新cmp -l / radiff2 / vbindiff。ずれる場合はブロック/構造単位で。変化区間をメモリマップに重ねて意味を推定
変化した区間がどの領域かを見て意味を推定。小さな連続=カウンタ、ブロック境界=設定/ログ

4. リバースエンジニアリングとしての差分

操作を 1 つだけ変えて、前後をダンプする——これは未知の構造を解く最強の手法である。

例: NVM3 / NVS(10 章・11 章)で、あるキーの意味を知りたい。

  1. 設定を既定にして全体をダンプ(before)
  2. その設定を 1 つだけ変える(例: 送信出力を 10 → 15)
  3. 再度ダンプ(after)
  4. 差分を取ると、変わった数バイトが、その設定の格納場所。値も 10→15 に対応して変わっている

これを設定項目ごとに繰り返せば、ドキュメントのないストレージの地図が埋まる。

5. 壊れたダンプ — まず切り分ける

ダンプが「理屈に合わない」とき、原因は 3 つ。

原因兆候対処
読み出しの失敗2 回読むと違う(04 章)、特定区間だけ 0 / 0xFF読み直す。配線・電源・クロックを疑う
実際の破損(電源断・劣化)CRC / ハッシュが合わない、追記の途中で切れている破損として扱う(次項)
自分の解釈違い1 か所だけ辻褄が合わないエンディアン・オフセット・アラインメント・構造の版を疑う(最も多い)

「まず自分を疑う」のが鉄則である。「壊れている」と決める前に、エンディアン(03 章)、ロードアドレス(07 章)、構造体の版、パディングを確かめる。壊れているように見えて、実は読み方が違うだけのことが大半である。

6. 破損の扱い方

本当に壊れている場合の対処。

壊れたダンプ — まず切り分ける読み出しの失敗2 回読むと違う、特定区間だけ 0/0xFF。→ 読み直す。配線・電源・クロック実際の破損CRC/ハッシュが合わない、追記の途中で切れている。→ 破損として扱う自分の解釈違い(最多)1 か所だけ辻褄が合わない。→ エンディアン・オフセット・アラインメント・版を疑う「まず自分を疑う」壊れていると決める前に、エンディアン(3 章)・ロードアドレス(7 章)・構造体の版・パディングを確かめる。破損は CRC/ハッシュで確認、追記型は最後を捨て、冗長領域で救い、必要な部分だけ取れれば十分
読み出し失敗・実際の破損・自分の解釈違いを切り分ける。まず自分を疑う

7. CRC を合わせて検証する

構造の末尾の CRC(03 章)を、実際に計算して照合すると、「自分の構造の理解が正しいか」「壊れていないか」の両方を確かめられる。

import binascii
# 例: レコードの [0:len-4] が本体、末尾 4 バイトが CRC-32 だと仮説
body, crc_stored = rec[:-4], int.from_bytes(rec[-4:], "little")
crc_calc = binascii.crc32(body) & 0xFFFFFFFF
print("一致" if crc_calc == crc_stored else "不一致(構造か CRC 種別が違う、または破損)")

合わなければ、CRC の種別(CRC-16 か 32 か)、初期値、ビット反転、対象範囲を変えて試す(crcmod で総当たり)。それでも合わなければ破損を疑う。

8. 手を動かす

差分から意味を読む


この章のポイント