Binary Analysis 10 · NVM3 — Silicon Labs の設定ストレージを読む

Chapter 10

NVM3 — Silicon Labs の設定ストレージを読む

この章のゴール.

NVM3 が「追記だけで更新する鍵値ストア」であることを理解し、 ダンプからオブジェクトの列を拾い、同じキーの最新値を復元できるようになること。 なぜこの作りなのか(フラッシュの制約)を、10 章以降のログ・NVS にも通じる形で理解すること。

この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、CRC(03 章 8 節)、オフセット(03 章 1 節)、サイズ(04 章 7 節)、バージョン(08 章 3 節)

1. NVM3 とは何か

NVM3(Non-Volatile Memory version 3)は、Silicon Labs(EFR32 / EFM32、Zigbee・Thread・Bluetooth の SoC)が使う不揮発データストレージのライブラリである。 設定値・較正値・ネットワーク鍵・Zigbee のバインディングなどを、キー(20 ビットの番号)と値(バイト列)の組として、内蔵フラッシュの一区画に保存する。

PSA 編 13 章の ITS / PS が「PSA の標準ストレージ API」だったのに対し、NVM3 は「Silicon Labs 実装の中身」である。同じ機器で、PSA ITS の裏側が NVM3 になっていることもある。

2. 追記型の設計 — なぜそうなるか

09 章で見たように、フラッシュは「消去はブロック単位、書き込みは 1→0 だけ」という制約を持つ。 キー 0x4001 の値を更新するたびに、そのキーの場所を消して書き直すのは、消去ブロック全体を巻き込み、寿命を縮める。

そこで NVM3(と、11 章の NVS、12 章のログ)は共通の戦略を取る。

更新は「上書き」ではなく「新しい版を末尾に追記」。古い版は残したまま、最新の版だけを有効とみなす。

この設計が、解析にとって重要な意味を持つ。

古い値がフラッシュに残っている.

追記型なので、過去の設定値・過去の鍵・削除したはずのデータが、区画の中に履歴として残っていることが多い。 これは障害解析では宝(設定がいつ変わったか追える)だが、セキュリティ監査では危険(消したはずの鍵が読める、13 章)でもある。 「最新値」だけでなく「そのキーの全履歴」を拾えることを意識する。

追記型 — 更新は「上書き」ではなく「末尾に追記」キー 0x4001 を 3 回更新するとkey 0x4001 = 10古い(残る)key 0x4001 = 12古い(残る)key 0x4001 = 15最新 = 有効0xFF 空き後に書かれたものが有効なぜこの設計か(フラッシュの制約、9 章)同じ場所を消して書き直すと消去ブロック全体を巻き込み寿命を縮める。だから空き場所に追記し、区画が埋まったら有効な最新版だけ集めて古いブロックを消す(ガベージコレクション)古い値・削除したはずのデータが履歴として残る(障害解析の宝、監査の危険。13 章)
更新は末尾に追記、最新版を有効とする。古い値が履歴として残る。NVM3・NVS・ログ共通

3. まず公式ツールを試す

01 章の原則どおり、自分で解く前に Simplicity Commander を試す。

commander nvm3 parse nvm3_dump.bin --tokengroup all
# または device から直接
commander nvm3 read --device EFR32MG21 --outfile nvm3.bin

これが動けば、キーと値の一覧が得られる。動かない(ダンプが部分的、区画オフセットが不明、壊れている)ときに、以下の手動解析に進む。

4. NVM3 の構造

NVM3 の区画は、ページ(フラッシュの消去ブロックに対応、通常複数)に分かれ、各ページはページヘッダの後ろにオブジェクトが詰まっている。

ページヘッダ

各ページの先頭に、そのページの世代を示す消去カウンタ(erase count)やバージョンが入る。 リペイジングのたびにカウンタが増えるので、どのページが新しいかが分かる。

オブジェクト

各オブジェクトは、おおむね次の形のヘッダ + データである(バージョンで詳細は異なる)。

部分内容
オブジェクトヘッダキー(20 ビット)、型(データ / カウンタ / 削除マーカ)、長さ、フラグ
データ値そのもの(小さい値はヘッダに埋め込まれることも)
(末尾)CRC など整合性の情報

オブジェクトは4 バイト境界に揃えて次々と追記される。だから区画を先頭から順にたどれば、オブジェクトの列を復元できる。

NVM3 の構造 — ページとオブジェクトページ(消去ブロックに対応)ページヘッダ消去カウンタ=世代objobjobj0xFF…空きオブジェクトヘッダキー(20bit)・型・長さデータ値CRC読み方: ① Commander nvm3 parse をまず試す ② ページヘッダの消去カウンタで新旧 ③ オブジェクトを 4 バイト境界で連続パース ④ 0xFF でページ終わり ⑤ 同じキーは最新を採用ヘッダのビット割り当ては NVM3 のソース(nvm3_*.c)が仕様書。キーの意味は SDK のトークン定義Zigbee / Thread / BLE スタックは決まったキー範囲を鍵・アドレスに使う
ページ(消去カウンタで世代)にオブジェクト(キー・型・長さ)が 4 バイト境界で連続

5. 手で拾う手順

公式ツールが使えないときの、原理どおりの読み方。

  1. 区画の範囲を特定する。フラッシュ全体のどこが NVM3 か(メモリマップ、strings で近くにヒントがないか、規則的なページ境界)
  2. ページヘッダを見つけ、消去カウンタを読む。ページサイズ(消去ブロック、多くは 8 KB / 2 KB)ごとに区切る
  3. 各ページで、ヘッダの後ろからオブジェクトヘッダをパースし、キー・型・長さを読み、長さ分進んで次のオブジェクトへ。4 バイト境界に整列
  4. 0xFF が続くところがそのページの空き(未使用)。そこでページ終わり
  5. 全オブジェクトを集め、同じキーは「後に書かれたもの(新しいページ・後ろの位置)」を最新とする。削除マーカがあれば、そのキーは削除済み
# 概念コード(実際のヘッダ形式はバージョンに合わせる)
def parse_page(page, page_base):
    objs, off = [], HEADER_LEN
    while off < len(page):
        if page[off:off+4] == b'\xff\xff\xff\xff':   # 空き
            break
        key, otype, length = parse_obj_header(page, off)   # ← 要実装
        val = page[off+HDR : off+HDR+length]
        objs.append((page_base+off, key, otype, val))
        off += align4(HDR + length)
    return objs

実際のヘッダのビット割り当ては、NVM3 のソース(nvm3_*.c、Silicon Labs が公開)を仕様書として読むのが確実である。ここでも「ライブラリのソースが最強の仕様書」(01 章)が効く。

6. キーの意味を知る

キーは 20 ビットの番号で、それ自体に意味はない。何のキーかは、そのアプリケーションが決める。

7. 障害解析での使いどころ

8. 手を動かす

追記型ストレージから最新値を復元する


この章のポイント