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"3. 何をファジングするか — 4 つの軸
BLE のファジングは、やみくもに乱数を送るより、軸を決めて攻めると効率がいい。
- 値の境界: 長さ 0 / 最大長 / 最大+1、0x00 と 0xFF、符号の境界、数値の最小・最大。MTU(一度に送れる最大バイト数) を超える長さ
- 型・書式: 数値を期待する所に文字列、区切り文字、NUL、制御文字。文字列にとても長い値(バッファ溢れ狙い、16 章)
- 順序(ステートマシン): 初期化前にコマンド、ペアリング前に秘匿値へ書き込み、同じ操作の二度打ち、途中で切断
- タイミング・量: 高速連投、無応答書き込みで溢れさせる、接続直後の集中砲火
4. 接続レベルの乱れに耐えるか
値だけでなく、接続そのものの異常に耐えるかも重要なブラックボックス観点である。
- 突然の切断(電波を切る/
disconnect)→ 機器がきれいに戻るか、無線が固まらないか - 再接続の連打: つないで即切る、を繰り返してリソース枯渇(14 章・16 章)が起きないか
- 並行接続: 複数のセントラルからつなぐ(対応台数を超える)と、既存接続が壊れないか
- ペアリング/ボンディングの拒否・失敗からの回復
これらは電流波形(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. 道具の地図
bleak(Python): 自作テスト・自作ファザの土台。クロスプラットフォーム- 専用ファザ: BLE 向けのファジングフレームワーク(例: 各種 OSS の BLE fuzzer)、汎用の
boofuzz(プロトコル記述型ファザ)を BLE トランスポートに合わせて使う - 低層: nRF52 系の開発ボード+専用ファーム(Sniffle など)で、正規スタックが弾く不正パケットを故意に送る(リンク層ファジング)
- 観測の併用: sniffer(Wireshark)+電流計+ログで「送った vs 起きた」を突き合わせる
7. 何をオラクルにするか
- 不正入力をきちんと拒否し(エラーコード)、クラッシュ・ハングしない
- 異常な順序・切断・並行接続から自力で回復する(既定状態に戻る)
- ファジング中も生存確認が通り続ける(落ちたら即検知)
- 溢れさせてもメモリ・電力が破綻しない(14 章・16 章と連携)
8. 手を動かす
GATT操作と簡易ファジング
この章のポイント
- 能動テストは「書いて反応を見る」。read/write/notify の組み合わせが基本
- ファジングは値の境界・型/書式・順序・タイミングの 4 軸で攻めると効率的
- 接続の乱れ(切断・再接続連打・並行接続)への耐性もブラックボックス観点
- 自作ファザの肝は生存確認——毎回「まだ生きているか」を確かめる
- 道具は
bleak+boofuzz、低層は Sniffle 等。sniffer・電流・ログと併用して突き合わせる