Embedded Test 10 · BLE ② 能動テストとファジング — つついて壊す

Chapter 10

BLE ② 能動テストとファジング — つついて壊す

この章のゴール.

状況把握(09 章)の次の段として、BLE を能動的にテストする——キャラクタリスティックに読み書きして 状態遷移を確かめ、境界値・不正値・異常な順序を送り込むファジングで堅牢性を試し、 接続の乱れ(切断・再接続・並行接続)に耐えるかを外から検証できるようになること。

この章で使う既出の用語(定義は各リンク先). オラクル(01 章 8 節)、判定(02 章 1 節)、状態遷移(02 章 1 節)、UART(05 章 1 節)、GATT(09 章 8 節)、アドバタイズ(09 章 2 節)、接続(09 章 2 節)

1. 受動から能動へ

09 章は「聞くだけ」だった。本章は書き込んで反応を見る。生成 AI が書いたファームは、正常な使い方(ハッピーパス)では動いても、想定外の入力や順序で簡単に転ぶことが多い。BLE はスマホから誰でも書き込める面なので、堅牢性の検証が特に重要になる。

ファジング(fuzzing) とは、大量の(しばしばデタラメな・境界的な)入力を自動で送り込み、クラッシュ・ハング・異常応答を探すテスト手法である。「入力の空間を機械的に踏み荒らして落とし穴を見つける」イメージ。

2. 能動テストの基本操作

bleak で GATT に対してできる操作は少なく、組み合わせが勝負になる。

操作意味テストでの使い方
readキャラクタリスティックの現在値を読む書き込みの結果確認、状態のオラクル
write(response 有)書いて確認応答を待つ正常系、エラー応答コードの確認
write(response 無)撃ちっぱなし高速連投・過負荷試験
start_notify機器からの自発通知を購読状態遷移・イベントの観測点
async with BleakClient(addr) as c:
    await c.write_gatt_char(CMD_UUID, b"\x01")      # コマンド送信
    await c.start_notify(EVT_UUID, lambda h, d: print("evt", d.hex()))
    val = await c.read_gatt_char(STATE_UUID)         # オラクル: 状態が期待通りか
    assert val == b"\x02"
接続後は read / write / notify の組み合わせで攻めるセントラル(PC / bleak)テスト側ペリフェラル(テスト対象)AI 生成ファームwrite(コマンド送信)read(値の確認・オラクル)notify(自発通知=観測点)書いた後は必ず read で「期待どおりの状態になったか」を確かめる
図10-1 能動テストの基本操作。

3. 何をファジングするか — 4 つの軸

BLE のファジングは、やみくもに乱数を送るより、軸を決めて攻めると効率がいい。

ファジングは 4 つの軸で攻めると効率がいい① 値の境界長さ 0/最大/最大+1、0x00・0xFF、MTU 超え、数値の最小・最大② 型・書式数値の所に文字列、区切り・NUL・制御文字、とても長い値(溢れ狙い)③ 順序(ステートマシン)初期化前・認証前の操作、二度打ち、途中で切断、あり得ない遷移④ タイミング・量高速連投、無応答書き込みで溢れさせる、接続直後の集中砲火
図10-2 BLE ファジングの 4 軸。

4. 接続レベルの乱れに耐えるか

値だけでなく、接続そのものの異常に耐えるかも重要なブラックボックス観点である。

これらは電流波形(03 章)や UART/SWO ログ(05 章・08 章)と同時に観測すると、「無線は切れたが内部はハングしている」といった外から見えづらい状態を捕まえられる。

5. 小さなファザを書く

専用ツール(後述)もあるが、bleak で数十行の自作ファザが十分に役立つ。基本形は「生成→送信→観測→判定」のループ。

import os, asyncio
from bleak import BleakClient

async def fuzz(addr, uuid, n=500):
    async with BleakClient(addr) as c:
        for i in range(n):
            length = [0, 1, 20, 255][i % 4]            # 境界長を巡回
            payload = os.urandom(length)
            try:
                await c.write_gatt_char(uuid, payload, response=True)
            except Exception as e:
                print("REJECTED", length, e)           # 拒否は正常な防御
            try:                                         # 生存確認: 一定時間内に応答するか
                await asyncio.wait_for(c.read_gatt_char(uuid), timeout=2.0)
            except Exception:                            # タイムアウト/切断=ハングとみなす
                print("HANG after", payload.hex()); break

生存確認(liveness check) が肝である。書き込んだあと「機器がまだ反応するか」を毎回確かめないと、いつ落ちたのか分からない。生存確認には、別のキャラクタリスティックの read、アドバタイズの再取得、電流波形の変化(03 章)などを使う。

6. 道具の地図

7. 何をオラクルにするか

8. 手を動かす

GATT操作と簡易ファジング


この章のポイント