Binary Analysis 03 · バイト列を読む基本 — エンディアン・整数・文字・マジック

Chapter 03

バイト列を読む基本 — エンディアン・整数・文字・マジック

この章のゴール.

16 進ダンプを見て、そこにある整数・ポインタ・文字列・マジックナンバー・タイムスタンプ・チェックサムを見分けられるようになること。 エンディアンとアラインメントという 2 つの落とし穴を理解すること。

1. 16 進ダンプの読み方

00008000: 0004 0020 c581 0008 8180 0008 8380 0008  ... ............
00008010: 8580 0008 8780 0008 8980 0008 0000 0000  ................

この 32 バイトは、後で見るように Cortex-M のベクタテーブルの先頭で、20000400(スタックの初期値)と 080081c5(リセット後に飛ぶアドレス)などが並んでいる。 なぜ 0004 0020 が 20000400 なのか——それがエンディアンである。

2. エンディアン — バイトの並び順

エンディアン(endianness)は、2 バイト以上の整数をメモリにどの順で置くかの規則である。

エンディアン — 0x20000400 をメモリにどう置くかリトルエンディアン(下位が先)00040020アドレス低 →→→ 高Cortex-M / x86 / RISC-Vビッグエンディアン(上位が先)20000400ネットワーク / 多くのファイルヘッダ見分け方: 小さな正の整数を探す「04 00 00 00」はリトルなら 4、ビッグなら 0x04000000(67108864)。4 のほうがありそうなら、その領域はリトル。xxd -e -g4(リトル)と xxd -g4(バイト順)を切り替えて意味の通るほうを選ぶ
組み込みはほぼリトル。ネットワークとファイルヘッダにビッグが混ざる。小さな整数で見分ける

組み込みで出会うのはほぼリトルエンディアンだが、次の場所ではビッグエンディアンが混ざる。

見分け方: 小さな正の整数(長さ、カウンタ、バージョン)を探す。 04 00 00 00 はリトルなら 4、ビッグなら 0x04000000(67108864)。4 のほうがありそうなら、その領域はリトルである。 xxd -e -g4(リトル)と xxd -g4(バイト順のまま)を切り替えて、意味の通るほうを選ぶ。

3. 整数を読む

見えるバイト(LE)u8u16u32
2a42——
2a 00—42—
00 04 00 20——0x20000400 = 536871936
ff ff ff ff255655354294967295(または −1 の i32、または「消去済み」)

符号の判断: 最上位ビットが 1 の 32 ビット値(0x80000000 以上)は、文脈によって「大きな正の数」「負の数」「アドレス」「フラグの集まり」のどれか。 温度・カウンタなど小さいはずの量が 0xFFFFFF80 なら、それは i32 の −128 である可能性が高い。

0xFF の連続は「消去済みフラッシュ」であることが多い。NOR フラッシュは消去すると全ビットが 1(0xFF)になり、書き込みは 1→0 の方向にしかできない(10 章)。だから FF FF FF FF は「まだ何も書いていない u32」を意味することが多く、数値の 4294967295 ではない。

4. アラインメント — 値は境界に揃う

コンパイラは、32 ビット値を4 の倍数のアドレスに、16 ビット値を 2 の倍数に置く(アラインメント)。 だから構造体には詰め物(パディング)が入る。

struct rec {
    uint8_t  type;     // オフセット 0
    // 3 バイトのパディング(コンパイラが挿入)
    uint32_t value;    // オフセット 4(0 ではない!)
    uint16_t len;      // オフセット 8
    // 2 バイトのパディング
};                     // 合計 12 バイト(10 ではない)

構造体を読むとき、「メンバのバイト数を足しただけ」ではズレる。 パディングを考慮するか、struct の書式に詰め物を明示する(Python なら <B3xIH2x。x が 1 バイトの詰め物)。 逆に、詰め物のない構造体(__attribute__((packed)) で作られたもの、通信フレーム、フラッシュに詰めた記録)もある。どちらかは実物で確かめる。

アラインメント — 値は境界に揃い、詰め物が入るstruct rec { uint8_t type; // オフセット 0 uint32_t value; // オフセット 4(1 ではない!) uint16_t len; // オフセット 8}; // 合計 12 バイト(7 ではない)メモリ上の並びtypepadpadpadvaluevaluevaluevaluelenlenpadpad0246810詰め物を無視して「バイト数を足しただけ」ではズレるパディングを考慮するか、struct 書式に明示(<B3xIH2x の x が詰め物)。通信フレームや packed 構造体は詰め物がない。実物で確かめる
32 ビット値は 4 の倍数アドレスに置かれ、間に詰め物が入る。バイト数の足し算はズレる

5. 文字列を読む

種類特徴例
ASCII(NUL 終端)1 バイト 1 文字、末尾に 0048 65 6c 6c 6f 00 = "Hello"
長さ前置先頭に長さ、その後に文字05 48 65 6c 6c 6f = 長さ 5 の "Hello"
固定長決まった幅に詰め、余りは 00 か空白"CFG1" を 8 バイト枠に
UTF-16 LE1 文字 2 バイト、英字は xx 0048 00 65 00 = "He"。Windows・BLE の一部
UTF-81〜4 バイト。ASCII 互換。日本語は 3 バイトe6 97 a5 = "日"

strings は既定で ASCII のみ。UTF-16 は strings -e l、UTF-8 の日本語は端末が UTF-8 なら xxd の右側や grep -a で拾える。

6. マジックナンバー — 形式の目印

マジックナンバー(magic number)は、ファイルやデータ構造の先頭に置かれた固定のバイト列で、「これは○○形式だ」と示す目印である。 解析ではこれを探すのが最初の一歩になる。

マジック(16 進)ASCII形式
7F 45 4C 46.ELFELF 実行ファイル(05 章)
1F 8B—gzip
42 5A 68BZhbzip2
28 B5 2F FD—zstd
FD 37 7A 58 5A—xz
89 50 4E 47.PNGPNG
55 AA / AA 55—ブートセクタ末尾、多くのヘッダの目印
E9 / EB—x86 ブートセクタ、FAT の先頭ジャンプ
hsqs / sqsh—squashfs(15 章)
85 19—JFFS2 ノード
39 6E 30 3B9n0;UBI
50 4B 03 04PK..ZIP / JAR / APK
D0 0D FE ED(0xd00dfeed BE)—デバイスツリー blob(DTB。15 章)
EF BE AD DE(0xdeadbeef)—よくあるデバッグ用の番兵値

MCUboot(0x96f3b83d、08 章)や NVM3(10 章)のように、マジックがファイル先頭ではなく、各構造の頭にある形式も多い。その場合は全域を検索する。

# MCUboot のマジック 0x96f3b83d をリトルエンディアンのバイト列で検索
grep -a -b -o -P '\x3d\xb8\xf3\x96' work.bin
# Python で全出現位置を得る
python3 -c "d=open('work.bin','rb').read(); i=-1;
import sys
while (i:=d.find(bytes.fromhex('3db8f396'),i+1))>=0: print(hex(i))"
マジックナンバー — 形式の目印を全域検索する.ELF 7F 45 4C 46ELF 実行ファイル(05 章)gzip 1F 8B圧縮(14 章)zstd 28 B5 2F FD / xz FD 37 7A 58 5A圧縮PK.. 50 4B 03 04ZIP / JAR / APKMCUboot 3D B8 F3 96(0x96f3b83d)各スロット先頭(08 章)hsqs / sqshsquashfs(15 章)D0 0D FE ED(0xd00dfeed BE)デバイスツリー(15 章)55 AA / EB / E9ブートセクタ・FAT の目印(09 章)マジックがファイル先頭でなく各構造の頭にある形式(MCUboot・NVM3)は全域を検索する
既知形式の先頭バイト列を探すのが最初の一歩。構造ごとに頭にある形式は全域検索

7. タイムスタンプを読む

日時が埋まっていると、ログの解析(12 章)や差分(18 章)で役立つ。

形式見え方変換
Unix 時刻(秒)2020 年代なら 0x6? 台の u32(例 0x65B00000)1970-01-01 からの秒。date -d @1706...
Unix 時刻(ミリ秒 / マイクロ秒)u641000 / 1000000 で割る
経過時間(tick)小さめの u32 が単調増加起動からの ms など。絶対時刻ではない
FAT 日時2 バイト日付 + 2 バイト時刻(ビット詰め)09 章

「2020 年代の Unix 秒」は 0x63000000〜0x70000000 の範囲(2023〜2029 年ごろ)に入る u32 を探すと当たりをつけやすい。

8. チェックサムと CRC を見分ける

構造の末尾にある、内容と相関しない 1〜4 バイトは、チェックサム(誤り検出の値)のことが多い。

CRC の照合は 18 章(壊れたダンプの検証)で使う。

9. 手を動かす

バイト列を各型として解釈する


この章のポイント