Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

Bluetooth通信実装のセキュリティ観点を4ステップ + 1で理解する

Bluetoothは、米国Bluetooth SIG,Inc.の商標です。

イントロ

こんにちは、株式会社Flatt Securityでインターンをしている@smallkirbyです。

最近はいろいろなものが無線化し、とても便利な世の中になっています。自分自身、無線イヤホンは手放すことができませんし、自宅もDIYでスマートホーム化しようとも考えています。最近では新型コロナへの対策としてスマートフォン及びビーコンを用いて接触情報や混雑状況を共有する仕組みも整備されてきています。世の中が便利になる反面、身近なものの情報化はセキュリティリスクが身近になることとトレードオフの関係にあります。

無線デバイスでよく使われている通信規格がBluetoothです。読者の皆さんの中には、Webで用いられる通信規格・プロトコルについては知っているけど、Bluetoothの仕組みは知らないという方も多いかと思います。本ブログではBluetoothを用いた通信を、ペアリングのセキュリティという観点に注目して掘り下げていこうと思います。

以下では、まずBLE通信で用いられるデータ構造であるGATTや実際の通信におけるペアリングフローについて概観した後に、Bluetoothのペアリングにおいて発生し得る5つの脆弱性について実際のPoCと共に紹介していきます。

なお、Bluetoothは大別してClassic BluetoothBluetooth Low Energy(BLE)が存在しますが、特に言及しない限り本ブログではBLEについて扱います。

BLE通信 概観

GATTプロファイル

BLE通信ではサーバ・クライアント方式と同様に、端末はperipheralcentralのいずれかの役割を持って通信を開始します。

ここでperipheralはサーバに相当します(eg: スマートロックの錠前、IoT側)。また、centralはクライアントに相当します(eg: スマートロックの鍵、スマートフォン側)。

BLEにおける通信は、GATT(Generic Attribute Protocol)というデータ構造を軸として行われ、peripheralは以下に持つようなGATTプロファイルから構成されています:

GATTプロファイルのイメージ

GATTサーバは1個以上のServiceを持っており、Serviceは0個以上のCharacteristicを持っています。このCharacteristicperipheralcentral間でやり取りされるデータの一つ一つを表しています。各Characteristicは0個以上のDescriptorを持っており、このDescriptorによってCharacteristicに更なるメタデータが付与されます。以上のServiceCharacteristicDescriptorは全て、attributeというデータ単位で構成されており、attributeには権限(permission)を設定することができます。権限には、readable/writableなどのIOの可否や、経路の暗号化が必須かどうか(encryption)等の情報を設定することができます。

ServiceCharacteristicは固有のUUIDを持ち、UUIDによって選択されます。UUIDは128bitの値ですが、送受信するデータ数を削減するために省略形も用意されており、Bluetooth SIGによってアサイン済みの特殊なUUIDも存在しています。

また、Characteristicには(attributeの権限とは別に)read/write/write-without-response/notify等のプロパティを指定することで、該当Characteristicに対して可能な操作を定義することができます。有効な属性・権限の詳細についてはCSv4.2 Vol3 PartG page 569等をご確認ください。

また、このGATTプロファイルやattributeの詳細については、O’Reillyのページに非常によくまとまっているため、興味がある方は参照してみてください。

GATTプロファイルの例として、以下に示すような"スマートロックシステム"を考えてみます1:

検証に利用するスマートロックシステムのGATTプロファイル

このプロファイルは1つのServiceを持ち、このServiceは以下の2つのCharacteristicを持っています:

  • Key Write Characteristic: UUIDはDEAD0002-DEAD-DEAD-DEAD-0080DEADBEEFcentralがドアの鍵を書き込むことでドアを開くことができる。
  • Door Status Characteristic: UUIDはDEAD0003-DEAD-DEAD-DEAD-0080DEADBEEF。 ドアの開閉状態を示す。notify可能。

また、Door Status CharacteristicCCCDと呼ばれるUUID: 29022の一般的なDescriptorを持っています。このDescriptornotifyindicateを有効・無効にするためのものです。notifyが有効になっている場合、ドアが開閉されてDoor Status Characteristicの値が変化すると、その変更がcentralに対して通知されるようになります。

このスマートロックシステムは、centralであるスマホが固定のドアの鍵をperipheralである錠前(peripheral)に書き込むことでドアを開閉することのできる単純な構成になっており、以下の図の流れで動作します:

本システムでのGATT通信の流れ

まず、centralperipheralに接続するとDoor Status CharacteristicのCCCDに書き込みを行うことで通知を有効にします。その後、Key Write Characteristicに対して固定のドアの鍵を書き込みます。peripheralは書き込まれた鍵が正規のドアの鍵であることを確認した後、正しければドアを開きます。続いてperipheralDoor Status Characteristicの値を変更し、変更をcentralに対して通知します。

以降の検証では、このスマートロックシステムにおけるperipheralcentralの両方をAndroidアプリとして実装したサンプルアプリを使います。実際のシステムではperipheralはドアにつけるIoTデバイスとして実装されます。このサンプルアプリは、以下の動画のように動作します(動画):

(動画)検証に利用するスマートロックAppの正常系

ペアリング

GATT通信自体は、ペアリングを行わずとも実行することができます。Androidの場合には、BluetoothDevice.connectGatt()を呼ぶことでペアリングを行わずにGATT通信を開始することができます。しかし、この状態で読み書きができるCharacteristicは暗号化(及び認証)が必要とされていないCharacteristicのみに限られます。

通信経路を暗号化するためには、ペアリング3というフローを踏む必要があります。ペアリングとは、peripheralcentralの間で鍵を生成・交換する処理のことを指し、その鍵を用いて以降の通信を行います。詳細については、後ほど解説します。

脆弱性 1: Characteristicの権限指定ミスによる平文通信

観点: GATT Characteristicと属性

以下では、BLEペアリングに関して発生し得る脆弱性を紹介していきます。

今回用いるスマートロックシステムにおいて、通信経路内の保護すべき情報Key Write Characteristicに書き込まれるドアの秘密鍵の値です(今回の実装ではこの値はperipheral及びcentral0xDEADBEEFCAFEBABEという値でハードコーティングされています4)。

Androidでは、以下のようにしてcharacteristicの生成及び権限の指定をすることができます。今回peripheral側では以下のようにKey Write Characteristicを定義しています(以降の章では都度この宣言を変更していきます):

private val chrKeyVal = BluetoothGattCharacteristic(
    UUID.fromString(chrKeyValUUID),
    BluetoothGattCharacteristic.PROPERTY_WRITE,
    BluetoothGattCharacteristic.PERMISSION_WRITE)

この宣言では、権限をPERMISSION_WRITEとしています。これはPERMISSION_WRITE_ENCRYPTEDPERMISSION_WRITE_ENCRYPTED_MITMと異なり、このcharacteristicを読み書きするのに暗号化(ペアリング)が必要ないことを示しています5。つまり、centralperipheralに対して秘密鍵を送信する際、秘密鍵が平文の状態で送信されます

みなさんもご存知のとおり、Bluetoothは電波に情報を載せて通信するため、受信範囲内の全てのデバイスは通信を傍受することが可能です。よって、上の例のようにcharacteristicに適切な権限を付与しないまま通信してしまうと、容易に秘密鍵が盗聴されてしまいます。

実際にこの場合の通信を観測してみると以下のように、扉を開けるためにcentralからperipheralに対して送信された秘密鍵が露出していることが分かります(ドアの秘密鍵は0xDEADBEEFCAFEBABE):

平文で通信されるドアの鍵

ドアの秘密鍵を盗聴してリークした攻撃者は、centralと偽ってこの鍵を送信することで任意のタイミングであなたの家のドアを開けることができてしまいます。

対策: characteristicへの暗号化必須属性の付与

このケースにおける問題点は明らかです。Bluetooth通信は誰でも傍受することができます。

よって、秘匿する必要のあるデータ(ここではドアの秘密鍵)を送受信する場合には、例外なくcharacteristicに暗号化必須属性を指定する必要があります。

脆弱性 2. Legacy Pairingにおける暗号化された通信のブルートフォース

前節では通信経路上に秘匿情報が載せられる場合、characteristicにはencryption required(暗号化必須)権限を指定しなければならないことを確認しました。これは、通信の際に必ずペアリングをするということと同義です。

ペアリングとは、peripheralcentralの間で鍵を交換し、その鍵を用いて通信経路を暗号化することを指します6

さて、ペアリング方式にはAssociation Modelと呼ばれる4つの方法があります:

  • Numeric Comparison: 両端末に共通の6つの数字を表示し、同一であることを確認させる(後述するSecure Connectionのみ)
  • Just Works: Numeric Comparisonにおいて固定の数字を利用し、両端末にはその数字を表示しない
  • Passkey Entry: 片方の端末で6つの数字(PIN)7を表示し、もう片方の端末でその数字を入力させる
  • Out of Band(OOB): BLEと関係のない経路で鍵を渡す

みなさんが頻繁に使うのは、OOB8を除く3つだと思います。 このうちJust Worksはペアリングすることを告げるダイアログを除いてユーザインタラクションを必要としないため、ユーザがペアリングしていることを意識する必要があるのはPasskey EntryNumeric Comaparisonの2つです。

Passkey Entryは、以下のように片方の端末に表示されたコードをもう片方の端末に入力することでペアリングを行う方式です:

Androidにおけるペアリングダイアログ
PCにおけるペアリングダイアログ

では経路を暗号化するためにKey Write Characteristicの権限を暗号化必須(Androidの場合PERMISSION_WRITE_ENCRYPTED)として通信を行ってみます。

以下はペアリングを開始した直後にperipheralcentralの間でやり取りされたパケットリストの一部です:

ペアリングしてるからもう安心...?

centralがイニシエータとなってペアリングが開始し、両端末が順に鍵情報の送信・鍵の整合性の確認を行った後、暗号化が開始(LL_START_ENC_REQ)されています。これ以降の通信は暗号化されており、リンクレイヤ9で意味のある通信を観測することはできません。

これで通信経路は暗号化されて万事安全...。と思いきや、この通信経路には脆弱性があります。

(後出しになりますが、)BLEにおけるペアリングには、Association Modelとは別にそもそもの鍵交換方式として、v4.0で導入されたLE Legacy Pairingとv4.2で導入されたSecure Connection (SC)の2つが存在します。

Legacy Pairingでは、BLE独自の鍵交換方式を用いて鍵を生成・交換し経路を暗号化しているのに対し、Secure ConnectionではDH法による鍵交換方式を用いています。

Legacy Pairingでは、以下のフローで鍵が生成・交換されて暗号化が開始されます(CSv4.2 page 663 Vol.3 Part H page 663より引用):

Legacy Pairing, Passkey Entryでのペアリングフロー (CSv4.2 page 663 Vol.3 Part H page 663)

今節の続く部分では、Legacy Pairingの仕組みとLegacy Pairingが持っている脆弱性について確認していきます。

LE Legacy Pairingにおける鍵生成と鍵交換

(クリックするとLegacy PairingにおけるSTK生成までのフローを説明します。)

Legacy Pairingでは、鍵生成及び交換がBLE独自の交換方式に則って行われます。Legacy PairingPasskey Entryにおける経路暗号化がどのようなフローで行われるかを順に追ってみましょう。

なお、暗号通信の開始要求を送る側をMaster、答える側をSlaveと呼ぶことがありますが、以下では前者をイニシエータ、後者を非イニシエータと呼ぶこととします。

TKの生成

最初に経路を暗号化するために使われる128bitの鍵のことをSTKと呼びます。このSTKperipheralcentralで共通です。STKTKという128bitのやはりperipheralcentralで共通の値をパラメタとして生成されます。

TKの交換方法はAssociation Modelに応じて異なります。Passkey Entryの場合には、片方の端末に表示された6桁の数字を、ユーザがもう片方の端末に入力することで通信経路上にTKが載ることなくTKを共有することができます

TKは、Passkey Entryの場合Passkeyを単純に128bitに拡張した値になります。例えばPINが884933であった場合、これを16進数128bitに拡張した0x000000000000000000000000000D80C5TKとなります。

random値の生成

Passkey Entryでは、ユーザ(central)がperipheralを認証するための仕組みを提供します。ユーザはperipheralcentralに対して同じPINコードを入力し、両者が入力された値が同一であることを確認することで認証を行います。

但し、TKとして使われるPINをそのまま経路上に載せる訳にはいかないので、以下の方法でTKが同一であることを確認します。

イニシエータ側は128bitのMrandというランダム値と、それをもとにしてMconfirmという値を生成します。ここで、Mconfirmを生成する際のパラメタとして先程交換したTKが利用されます。

また、非イニシエータ側は128bitのSrandというランダム値と、それをもとにして128bitのSconfirmという値を生成します。ここで、Sconfirmを生成する関数の引数として先程交換したTKが利用されます。

その後、両者はMconfirm, Sconfirm, Mrand, Srandを順に交換し合います。この時点で両者ともにMconfirmまたはSconfirmを計算するためのパラメタが既知であるため、両デバイスは互いのMconfirm / Sconfirm値を再計算して交換した値と合致するかどうかを試すことで認証を完了させます。

STK/LTKの生成

両者が交換・再計算したconfirm値の整合性が確認された場合、両者はSTKを生成します。このSTKの生成には、Sconfirm,Mconfirm,TKの3種類のパラメタが用いられます。以降はSTKを用いて一時的に通信路を暗号化します。STKを用いて一時的に暗号化した経路を用いてLTKを交換し、このLTKによってそれ以降の経路を暗号化します。

観点: ペアリングフローの盗聴による経路復号

盗聴したLegacy Pairingの内容より、各交換パラメタは以下のようである10とわかったとします(これらのパラメタ交換自体は平文で行われるため、容易にリークできます):

  • TK: unknown (実際にはPINの0xD80C5だが経路に載らない)
  • Mconfirm: 3dc393920b9d2cfbea70ad130490a032
  • Sconfirm: 3cc8c0379cf6a7856d46b475c6787efc
  • Mrandom: d7690095e642c2666066089f67cbe376
  • Srandom: 1a415e992bae10ecf3447d11851aa4b2
  • preq: 0f0f102d000401 (ペアリングリクエストのパケット本体)
  • pres: 05071005000102 (ペアリングレスポンスのパケット本体)
  • ia: 7d68d5e3c0c3 (イニシエータアドレス)
  • ra: 44e5171465f3 (非イニシエータアドレス)
  • iat: 1 (イニシエータアドレスタイプ)11
  • rat: 0 (非イニシエータアドレスタイプ)

このとき、STKを求めるために必要な未知情報は、TK(すなわちPINコード)のみです。但し、上述したとおりTK(PIN)は10進6桁の値であるため、せいぜい20bit程度のエントロピーしか持っていません

しかも、TKが正しいかどうかは既知のconfirm値とrandom値を用いて検算することができます。よって、このTKはブルートフォース(総当り)によって計算することが可能です。

以下は、未知のTKをブルートフォースによって計算するサンプルプログラムです(暗号関数の厳密な定義はCSv4.2 page 594, CRYPTOGRAPHIC TOOLBOXにあるため、気になる方は参考にしてみてください):

/*
 *  Written by Flatt Security, Inc.   @smallkirby
 */

use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
use aes::Aes128;

// MSB of `v` corresponds to the first byte of return value.
fn u128_to_array(v: u128) -> [u8; 16] {
  let mut ar = [0u8; 16];
  for i in 0..ar.len() {
    ar[i] = ((v >> (128 - ((i + 1) * 8))) & 0xFF) as u8;
  }
  ar
}

fn block_to_u128(ar: &[u8]) -> u128 {
  let mut v: u128 = 0;
  for i in 0..(128 / 8) {
    v += (ar[i] as u128) << (128 - ((i + 1) * 8));
  }
  v
}

fn change_endian_u128(v: u128) -> u128 {
  let mut v_rev = 0;
  for i in 0..(128/8) {
    v_rev += ((v >> (128 - ((i+1)*8))) & 0xFF) << (i*8);
  }
  v_rev
}

// Generate 128-bit encrypted data using AES-128-bit block cypher as defined in FIPS-197.
fn e(_key: u128, _plaintext: u128) -> u128 {
  let key = GenericArray::from(u128_to_array(_key));
  let mut block = GenericArray::from(u128_to_array(_plaintext));

  let cipher = Aes128::new(&key);
  cipher.encrypt_block(&mut block);
  block_to_u128(block.as_slice())
}

// Generate STK for LE Legacy Pairing
fn s1(k: u128, r1: u128, r2: u128) -> u128 {
  let r1_dash = (r1 << 64) >> 64;
  let r2_dash = (r2 << 64) >> 64;
  let r_dash = (r1_dash << 64) | r2_dash;
  e(k, r_dash)
}

// Generate confirm value for LE Legacy Pairing
fn c1(
  k: u128,
  r: u128,
  preq: u64,
  pres: u64,
  iat: u8,
  rat: u8,
  ia: u64,
  ra: u64,
  padding: u32,
) -> u128 {
  assert_eq!(padding == 0, true);
  let p1: u128 = (((pres as u128 & 0xFFFFFFFFFFFFFF) as u128) << (128 - 56) as u128) as u128
    + ((preq as u128 & 0xFFFFFFFFFFFFFF) << (128 - 56 - 56)) as u128
    + ((rat as u128) << (128 - 56 - 56 - 8)) as u128
    + ((iat as u128) << (128 - 56 - 56 - 8 - 8)) as u128;
  let p2: u128 = ((ia as u128) << 48) as u128 + (ra) as u128;
  let tmp = e(k, r ^ p1) ^ p2;
  e(k, tmp)
}

// Generate Mconfirm 128bit value.
fn gen_mconfirm(tk: u128, mrand: u128, preq: u64, pres: u64, ia: u64, ra: u64) -> u128 {
  c1(tk, mrand, preq, pres, 1, 0, ia, ra, 0)
}

// Brute-force to reveal STK.
fn crack(mconfirm: u128, mrandom: u128, preq: u64, pres: u64, ia: u64, ra: u64) -> Option<u128> {
  for _tk in 0..=999999 {
    let tk = _tk as u128;
    let cur_mconfirm = gen_mconfirm(tk, mrandom, preq, pres, ia, ra);
    if cur_mconfirm == mconfirm {
      return Some(tk);
    }
  }
  None
}

fn main() {
  let mconfirm = change_endian_u128(0x3dc393920b9d2cfbea70ad130490a032);
  let mrandom = change_endian_u128(0xd7690095e642c2666066089f67cbe376);
  let preq = 0x0f0f102d000401;
  let pres = 0x05071005000102;
  let ia = 0x7d68d5e3c0c3;
  let ra = 0x44e5171465f3;

  println!("Cracking...");

  match crack(mconfirm, mrandom, preq, pres, ia, ra) {
    Some(tk) => {
      println!("TK found: {tk}");
    }
    None => {
      println!("failed to find TK.");
    }
  }
}

実行結果は以下のようになり、通信経路を盗聴することでリークした既知の値から、未知のTKの値を逆算できていることがわかります:

リークした情報からTK(PIN)が計算できる

導出したTK0xD80C5 == 633624であり、ユーザが入力したPINと一致します。

このようにLegacy Pairingにおいては、Passkey EntryJust Worksのいずれを用いたとしても、その通信経路は復号されてしまいます

但し、このようにして復号可能なのは攻撃者がペアリングフローの初期段階(Pairing RequestからPairing Randomまで)を盗聴している場合に限ります。その点さえ盗聴されていなければ、上記の方法でTKを求めることはできません。

これらの事実はCSv4.2においても以下のように述べられています:

For LE Legacy Pairing, none of the pairing methods provide protection against a passive eavesdropper during the pairing process as predictable or easily established values for TK are used. If the pairing information is distributed without an eavesdropper being present then all the pairing methods provide confidentiality. (CSv4.2, page 606) (筆者訳: LegacyPairingにおいて、TKとして使われるPINは容易に推測・計算可能な値が使われており、故にペアリングの初期段階から行われるpassiveな盗聴に対してはどのメソッドも脆弱である。ペアリングの鍵交換時点において盗聴者が存在しない場合には、全てのメソッドにおいて通信内容は秘匿される。)

既成ツールを用いたTKの総当りと通信の復号実践

passiveな盗聴による経路復号のイメージ図

上記のサンプルコードでTKを総当りで求めることができました。TKさえ求めれば、STKは専用の関数に入れるだけで瞬時に求まり、そのSTKを用いて経路を復号することでLTKをリークし全ての通信を復号することができてしまいます。

TKの総当りからpcapファイルの復号までを自動で行ってくれるライブラリとして、CrackLEというものがあります。CrackLEを用いて先程の暗号化されたpcapファイルを復号すると、以下のようになります:

CrackLEによるTK計算からpcapファイルの復号まで

ブルートフォースによってTKを計算し、そこからさらに経路の暗号化に使われるLTKを算出できていることがわかります。このLTKを用いて経路を復号することで、攻撃者はやはり秘密鍵を入手して自由にドアを開けられるようになりました。

対策: Legacy vs Secure Connection

さて12、ここで問題となっていたのはLegacy Pairingを使うことで例えペアリングをしていたとしても攻撃者が通信を復号できてしまうことでした(このような盗聴のことを、passiveな盗聴と呼びます)。

解決方法としては単純で、Secure Connectionを使えばいいということになります。

但し、Secure Connectionをサポートしていない(Bluetooth v4.2以前)端末と通信をする場合には当然Secure Connectionを使うことはできません。この場合は、次善の策として後述の脆弱性 5の対策にもあるように、アプリケーションレイヤに独自の暗号化を入れる等の対策が考えられます。

ここで、Legacy PairingSecure Connectionのどちらが使われるかは、何によって決められるのでしょうか。

これはBLEの仕様で決められており、Pairing Request及びPairing Responseで交換される情報の中でperipheralcentralSecure Connectionをサポートしているかどうかを表すフラグを持っており、両者がSecure Connectionをサポートしている場合にはSecure Connectionでペアリングが行われるようになっています13

Androidの場合には、現在確立されている接続がLegacy PairingなのかSecure Connectionなのかを知るための公開APIは勿論、hidden APIも見つかりませんでした(ご存知の方はご教授ください)。後述するように、どのAssociation Modelがペアリング時に使われているかを知る方法は(若干dirtyながらも)用意されていますが、Androidにおいては現状LegacyとSCのどちらを使って接続しているかどうかは分からないようになっていると思われます

そのため現実策としては、少なくともperipheral側のIoTデバイスで現在の接続がSecure Connectionを利用しているかどうか確認できるようにするとともに、centralとのペアリングでSecure Connectionの利用を強制することで、Legacy Pairingの利用とそれに伴うpassiveな盗聴を防ぐことが可能です14

脆弱性 3. Secure ConnectionのJust Worksにおけるperipheralのspoofing

4種類のAssociation ModelJust Worksの脆弱性

さて、では先程の反省を活かしてKey Write Characteristicは暗号化必須とし、さらにperipheral側でもSecure Connectionを使うようにしたとしましょう。これで通信路は確立されて、passiveに盗聴されたとしてももう安全!...と思いきや、やはりまだこの通信には脆弱性が存在し得ます。

先程Secure Connectionでのペアリング時に利用するAssociation Modelは4種類あると言いました。以下、OOBを除いた各モデルにおける鍵交換時の違いに注目して再度軽く説明します(Secure ConnectionにおけるPIN自体は暗号化のために使われることはなく、あくまでも認証のために利用されます):

  • Numeric Comaprison: 交換された公開鍵とnonceをもとに計算される6桁のPINが両デバイスに表示され、ユーザが両デバイスに表示された値が同一であることを確認することで接続相手が確かにユーザの意図するものであると認証することが可能
  • Just Works: Numeric Comparisonと同じだが、PINの表示による認証を行わず、ユーザインタラクションはない
  • Passkey Entry: ランダムに生成された6桁のPINを片方(若しくは両方)のデバイスに入力することで、接続しようとしているデバイスがユーザの意図するものであると認証することが可能

上記の説明からも分かるとおり、Numeric ComaprisonPasskey Entryではユーザが端末を認証することが可能となっています。しかしながら、Just Worksにおいては認証のためのユーザインタラクションがないため、ユーザ(central)が接続しようとしているperipheralを正規のものかどうか認証することができません

これが3つ目の問題点となります。

観点: Just Worksにおけるperipheral spoofing

JustWorksにおけるperipheral spoofingのイメージ

両端末がJust Worksで接続する場合には、以下のようにして攻撃を行うことが可能です。

まず、攻撃者は正規のperipheralのローカルネームやService UUIDを調べます。これは正規のデバイスが発している広告(Advertisement)を受け取ればよく、Advertisementは誰でも(電波の届く限り)取得可能なので容易に実行できます。

なお、centralを騙すためにローカルネームが必要かどうかはcentralのデバイススキャンの実装に依存します。今回のAndroid centralの場合には以下のように接続相手をフィルタリングしており、Service UUIDとローカルネームさえ分かれば良いことが確認できます(他にはmanufacture codeをスキャン対象に入れること等が考えられます):

private val scanFilter = ScanFilter.Builder()
    .setServiceUuid(ParcelUuid(UUID.fromString(srvDoorUUID)))
    .setDeviceName("DOOR")
    .build()

これらの情報を奪取した後、攻撃者は正規のperipheralが持つService/CharacteristicのUUIDを持つGATTサーバを立ち上げます。

今回はUbuntu(Linux v5.16.9)PC上でdbus APIを操作することで偽のGATTサーバを建てました15

今回使っているスマートロックシステムにおいて、centralはペアリング成功後にWrite Key Value Characteristicにドアの鍵を書き込むようになっているため、攻撃者側のGATTサーバではこのCharacteristicの実装が必要条件になります。攻撃者側のGATT characteristicは以下のように実装されています:

# Key-write characteristic
class ChrKey(Characteristic):
  def __init__(self, bus, index, service):
    Characteristic.__init__(
            self, bus, index, DOOR_UUIDS.ChrKeyValUUID.value,
            ['write-without-response', 'encrypt-write'], service)

  def WriteValue(self, value, options):
    logger.info(f"Write request to ChrKey: value={value}")
    val = dbus2bytes(value)
    self.processReceivedKey(val)

  def processReceivedKey(self, key):
    key = u64(key, endian="big")
    logger.info("[!] Secret key is leaked: {}".format(hex(key)))

その後、ユーザが正規のアプリを用いてドアデバイスを検索します。

ここでは正規のperipheralと攻撃者が建てている偽のperipheralの両方が存在するため、どちらに接続するかは運試しになります(厳密にはAndroidの場合には以下のように接続ストラテジーを選択することができるため、peripheralとしてどの端末を選択するかは実装依存になります):

private val scanSettings = ScanSettings.Builder()
  .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
  .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) // <-- scan match strategy
  .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
  .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
  .setReportDelay(0)
  .build()

ユーザが攻撃者の方のperipheralに接続を行うと、通常通りJust Worksでペアリングが行われます。

この際Just Works方式では他の2つのモデルと異なり認証ステップがないため、ユーザのスマホには「ペアリングしますか?」というダイアログのみが表示されることになります16。そうしてユーザは攻撃者のperipheralに接続していることに気が付かないままペアリングは完了し、ドアの秘密鍵を攻撃者のperipheralに送信してしまうことになります。

経路は確かに暗号化されていますが、接続先が攻撃者であるため、もはや経路暗号化はなんの意味もありません。

以上の攻撃を実際に行うと、以下の動画のようになります17:

(動画) peripheral spoofingによるドアの鍵奪取PoC

peripheralがドアをサーチした後、攻撃者(画面右ターミナル)に対して接続し、ペアリングを行っていることがわかります。ペアリングはJust Worksで行われるため、ユーザには"Pair with DOOR?"というダイアログのみが表示され、PINコードによる認証は行われていません。

攻撃者はここで表示されるローカルネームも偽っているため、ユーザは接続先が攻撃者であることに気がつくことができず、そのまま偽のperipheralに秘密鍵を書き込んでしまい、攻撃者はターミナル上でリークした秘密鍵を確認することができています([!] Secret key is leaked: 0xdeadbeefcafebabe)。この間、本物のcentralは一切関与していません18

こうして、やはり攻撃者にドアの鍵が奪取されてしまいました。悲しいですね。

対策: IO capabilityとAssociation Model

さて、ここでの問題はSecure Connectionを使っていてもJust Worksを使ってペアリングをするとユーザ(peripheral)の接続先が正規のperipheralであるかが認証できず、偽のperipheralに対して秘密情報を送信してしまう可能性があるということでした。

対策としては、Just Works以外のモデルとしてNumeric ComparisonPasskey Entry(またはOOB)を使うということになります。

しかし、ペアリング時に用いられるAssociation Modelはどのようにして決定されるのでしょうか。

これは、両デバイスが保持するIO capabilityによって決定されます。IO capabilityとは端末が持つ入出力機能のことで、以下の5種類が定義されています(CS v4.2 vol3 partH page607):

  • DisplayOnly: PINコードを出力することのできるディスプレイ機能を有する(スマホの画面・LCDディスプレイ等)のみを持ち、入力機能を有さない。
  • KeyboardOnly: 出力機能を有さないが、PINコードの入力機能を持つ(無線キーボード等)
  • DisplayYesNo: 出力機能を持ち、Yes/Noの入力機能を持つが、PINコードの入力機能は持たない
  • NoInputNoOutput: 入出力機能を一切持たない(無線イヤホン等)
  • KeyboardDisplay: 入出力機能をともに持つ(PC等)

たとえばKeyboardOnlyperipheralKeyboardDisplaycentralがペアリングしようとしたとします(これは、HHKB等の無線キーボードとPCを接続しようとする場合に当てはまります)。

この場合、PasskeyEntryを選択してPCに出力されたPINをキーボードに打ち込むということは可能ですが、両デバイスにPINを表示する必要のあるNumeric Comaprisonは利用することができません。このように、両端末のIO capabilityに応じてペアリングで使用するモデルがマッピングされています(CS v4.2 vol3 partH page 610-611より引用):

IO cap mapping 1
IO cap mapping 2

上のマップからも分かるとおり、ペアリングモデルはIO capabilityの劣っている方に合わせて選択されることになります。そして、この交渉はペアリングを開始するPairing Request / Pairing Responseのパケット中で相手に対して通知されます:

Pairing RequestにおけるIO capabilityの交換

本システムのPairing Requestを送る側(central)はAndroid端末であるため入出力機能をともに有しており、上の例では確かにIO CapabilityとしてKeyboard, Display (0x04)が通知されています。

よって、Just Worksで偽のperipheralに接続される問題に対しては、peripheralにPasskey EntryNumeric Comaprisonを行うための入出力機能を実装し、ペアリングリクエスト時に自身のIO capabilityを相手に通知するということが対策となります。

脆弱性 4: IO capability spoofingによるdowngrade攻撃

観点: IO capabilityのspoofing

さて、Characteristicに暗号化必須の属性をつけ、Secure Connectionを使い、さらにperipheralにはちゃんとディスプレイかキーインプットをつけてJustWorks以外で認証もちゃんと行うようにしたとしましょう。これでもう、盗聴もされないしperipheralを詐称される恐れもなくてもう安全!... と思いきや、まだこの実装には脆弱性があります。ここで残っているのは脆弱性 3と同じ、peripheralの詐称(spoofing)です。

HTTPS通信におけるダウングレード攻撃を聞いたことがあるでしょうか。TLSにおけるダウングレード攻撃は、通信開始の際に中間者が通信に介在して通信を改ざんすることで弱い暗号スイートの使用を強制することを指します。そして、BLE通信においてもこれと似たようなダウングレード攻撃が成立します。

IO capability spoofingのイメージ

以下は、攻撃者がやはりperipheralを偽ったGATTサーバを構築し、centralを接続させようとしている場合を想定します19

先程、ペアリングで使用するAssociation Modelは通信の開始時におけるIO capabilityの交換によって決定されることを確認しました。ここで、攻撃者が敢えて低いIO capabilitycentralに応答したとします

例えば自身のIO capabilityNoInputNoOutputと偽ってPairing Responseを返したとすると、上のマッピングより利用されるモデルはJust Worksになってしまいます。先程確認したとおりJust Worksは端末の認証ができないためユーザはやはり接続先が攻撃者のサーバであることに気がつかずに接続して秘密鍵を書き込んでしまうことになります。

ここで正規のperipheralは一切関与していないため、いくら正規のperipheralが入出力機能を有していたとしても関係なくJust Worksが使われることになってしまいます。

そして、攻撃者はこのIO capabilityを任意の値に指定することができます。Linuxにおいては、利用するIO capabilitybtmgmt(やbluetoothctl)等のコマンド(dbusが提供するAPIのラッパー)を利用して変更できます。

先程お見せした動画では以下のスクリプトを用いてIO capabilityNoInputNoOutputにし、通信にJust Worksを利用させるように強制しています:

set_hci() {
  sudo rfkill unblock bluetooth
  sudo btmgmt -i $HCI power off
  sudo btmgmt -i $HCI bredr off
  sudo btmgmt -i $HCI sc on
  sudo btmgmt -i $HCI le on
  sudo btmgmt -i $HCI bondable on
  sudo btmgmt -i $HCI pairable on # same with bondable
  sudo btmgmt -i $HCI connectable on
  sudo btmgmt -i $HCI discov yes
  sudo btmgmt -i $HCI advertising off
  sudo btmgmt -i $HCI io-cap 3
  sudo btmgmt -i $HCI power on
}

なお偽のGATTサーバにおいては、もはやCharacteristicを暗号化必須にする必要さえもなく、そうすることでペアリングを行うことすらなく通信を行うことも可能です20

既存のBLEフレームワークの不備と対策

さて、この問題に対してはどう対処すればいいでしょうか。Zhang, Y.らの論文(References 5)にも書いてあるとおり、現代の主要なBLEフレームワークはBLE接続をセキュアにハンドリングしているとは言い難い状況です(この論文は2019年に書かれたものですが、現時点で私がソースコード等を軽く調べた感じ状況はあまり変わっていませんでした。アップデートをご存知の方はご教授ください)。

Androidを例として見てみます。

まず、AndroidはどのAssociation Modelを使うかどうかをデベロッパ側がハンドリングするAPIを提供していません。すなわち、「Passkey Entry以外のペアリングしかできないのであれば、ペアリングをしない」というような選択肢を取ることはできないようになっています。

但し、ペアリングが成功した(接続状況が変化した)際にはブロードキャストが発行されるため、以下のようにブロードキャストを購読することにより、確立したペアリングがどのAssociation Modelを使ったのかはペアリング後に知ることができます。

private val bondingBroadReceiver = object: BroadcastReceiver() {
  override fun onReceive(c: Context?, intent: Intent?) {
    val pairingMethod = intent?.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT", -1)
    val state = intent?.getIntExtra("android.bluetooth.device.extra.BOND_STATE", -1)
    Logger.i("Bond state changed: method=$pairingMethod current=$state")
  }
}
private val pairingReqBroadReceiver = object: BroadcastReceiver() {
  override fun onReceive(c: Context?, intent: Intent?) {
    val pairingMethod = intent?.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT", -1)
    val state = intent?.getIntExtra("android.bluetooth.device.extra.BOND_STATE", -1)
    Logger.i("Pairing requested: method=$pairingMethod current=$state")
  }
}
init {
  val bondingFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
  val pairingFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
  context.registerReceiver(bondingBroadReceiver, bondingFilter)
  context.registerReceiver(pairingReqBroadReceiver, pairingFilter)
}

なお、ペアリングメソッドを指定することができないというのはBLEの仕様的に仕方のない事ですが、流石にBLE-sig側もまずいと思っているらしくSecure Connection Only(SCO)モードというものが定義されています。SCOモードはperipheral側が要求できる接続モードで、Secure Connectionの利用とJust Works以外のAssociation Modelの利用を強制することができるモードです。

しかし、いくらSCOモードがperipheralで実装されていたとしても先に述べたようにcentral(スマホ)側でセキュアなペアリングメソッドが強制されていなければ、攻撃者がperipheralを偽ってスマホと接続することが可能になってしまいます。

また、Androidではボンディングが行われて保存されたLTKをデベロッパ側が削除する明確なAPIを提供していません21 。そのため、例え上の方法で脆弱なペアリングを検知して以降の通信を中止したとしても、ボンディングされたペアリング情報は保存されてしまうということになります。

なお、AndroidにおいてはBluetoothDeviceオブジェクトにremoveBond()というメソッドがあり、これを用いてLTKを削除することが可能になっていますが、これはprivateメソッドのため基本的にはデベロッパ側が呼び出すことはできません。

一応以下のようにリフレクションを使えばアクセスすることは可能です:

// assume that `connectingDevice` is type of `BluetoothDevice`
try {
    connectingDevice?.run {
        this::class.memberFunctions.find { it.name == "removeBond" }?.let {
            it.isAccessible = true
            if(it.call(this) == true) {
                Logger.d("Success removing bond information.")
            } else {
                Logger.d("Failed to remove bond information.")
            }
        }
    }
} catch(e: Exception) {
    Logger.e("Failed to remove bond information")
    Logger.e("$e")
}

ですが、基本的にリフレクションを使うことはあまり褒められたことではありませんし、そもそもnon-SDKインタフェースを利用することは最近のAndroidでは制限されてきています

対策: central側でのペアリングメソッドの確認

これらはAndroid側の問題であり、他のプラットフォームも同様の問題を抱えています。プラットフォームの問題である以上、アプリデベロッパー側が根本的な対策をするのは少々難しいですが、以下のような方法で対策を行うことが考えられます。

まず(もしもリンクレイヤのセキュリティに頼りたいのであれば)、上記のようにペアリング後に使用されたAssociation Methodを確認し、もしも開発者側が要求する強度以下のメソッドが使われていればそれ以降の通信を行わないという処理を実装することが考えられます22:

private val REQUIRED_PAIRING_METHOD = BluetoothDevice.PAIRING_VARIANT_PIN;
private var usedPairingMethod: Int? = null;

private val bondingBroadReceiver = object: BroadcastReceiver() {
  override fun onReceive(c: Context?, intent: Intent?) {
    val pairingMethod = intent?.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT", -1)
    state = intent?.getIntExtra("android.bluetooth.device.extra.BOND_STATE", -1)
    Logger.i("Bond state changed: current=$state")
    // check if used pairing method is Passkey Entry. Otherwise, abort communication.
    when (usedPairingMethod) {
      REQUIRED_PAIRING_METHOD -> Logger.i("Encrypted with method $REQUIRED_PAIRING_METHOD."),
      else -> doAbort(),
    }
  }
}

private val pairingReqBroadReceiver = object: BroadcastReceiver() {
  override fun onReceive(c: Context?, intent: Intent?) {
    usedPairingMethod = intent?.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT", -1)
  }
}

init {
  val bondingFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
  val pairingFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
  context.registerReceiver(bondingBroadReceiver, bondingFilter)
  context.registerReceiver(pairingReqBroadReceiver, pairingFilter)
}

void doAbort() {
  disconnect(); // disconnect from target GATT.
  removeCurrentBond(); // remove bonding information (non SDK way)
  showError(); // show error to user
}

また、初回のペアリングを終えてボンディング(LTKの保存)まですることで、次回以降はペアリングを行うことなく経路を暗号化することが可能になります。そのため初回のペアリングだけでもユーザ側に、近くに誰も居ない事や不審な端末が存在しないことを確認するよう注意を喚起するのも1つの手かと思います2324

追加の緩和策として、peripheralのデバイススキャン時に複数候補が見つかった場合にはエラーを表示するという方法も考えられます25。しかしながら、攻撃者が正規のperipheralに対して延々とペアリング要求を贈り続ける(且つ、peripheralがペアリング開始時に広告を停止する実装になっている)ような場合には、ユーザが検索可能なデバイスは攻撃者のperipheralのみになってしまいます(DoS攻撃)。

よって、やはりスキャンに依存する対策だけでは不十分であり、あくまでも先述の方法で対策を行った上での追加策と考えるべきかもしれません。

また、central側の対策として権限不足によるperipheralからのエラーをトリガーとしてペアリングを開始するのではなく、central側から無条件でペアリング要求を行うことが考えられます。暗号化必須でないcharacteristicに対してもcentral側からペアリングを行うことで、攻撃者が偽のperipheralで暗号化無しで通信を行おうとしても必ずペアリングを行うことができます。これによって、characteristicを暗号化必須でない権限に設定にすることで平文通信を行わせるspoofing対する対策となります26。Androidの場合にはcreateBond()メソッドによって実現可能です。

脆弱性 5: 正規アプリが確立した接続の悪用

観点: 同一Android端末におけるnotify通知

Notification sniffingのイメージ

さて、最後にペアリングからは少し違う観点の脆弱性を一つ紹介します。

先程から少し登場していますが、GATTにはnotify/indicateという仕組みがあります。これは、Read属性のあるCharacteristicに対して購読を行う機能で、Characteristicの値に変更があった場合にperipheral側がcentralに対して変更を通知してくれるというものです。

この通知に対してcentralがレスポンスを返すものがindicate、レスポンスを返さないものがnotifyと呼ばれます。購読はCCCDと呼ばれる固定UUIDのDescriptorに対して特定の値を書き込むことで開始することが可能です。

今回のスマートロックシステムのAndroid centralでは以下のような実装でドアの開閉状態を購読しています:

// `chr`は`Door Status Characteristic`を表す`BluetoothCharacteristic`型変数
private fun subscribeDoorStatus(chr: BluetoothGattCharacteristic) {
    val cccd = chr.getDescriptor(UUID.fromString(CCCDUUID))
    if (cccd == null) {
        Logger.e("Failed to find CCCD of chr: ${chr.uuid}")
        return
    }
    Logger.i("Subscribing to chr: ${chr.uuid}")
    connectedGatt?.setCharacteristicNotification(chr, true)
    cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
    connectedGatt?.writeDescriptor(cccd)
}

これでDoor Status Characteristicに変更があった場合にcentral側で変更を知ることができます。

このnotifyですが、同一Android端末内で複数のアプリが同じCharacteristicを購読していた場合どうなるでしょうか。この場合、購読している全てのアプリにおいて通知が飛んできて読むことができます

例えば、今回のスマートロックシステムにおいて正規のcentralのスマホに悪意のあるアプリが入っていたと仮定してみましょう。悪意のあるアプリは以下のようにして実装されています:

private val gattCallback = object: BluetoothGattCallback() {
    override fun onCharacteristicChanged(
        gatt: BluetoothGatt?,
        characteristic: BluetoothGattCharacteristic?
    ) { // leak notification
        super.onCharacteristicChanged(gatt, characteristic)
        val newValue = characteristic.value.toHex()
        Logger.i("Notification: ${characteristic.uuid}($newValue")
        doorState.postValue(newValue)
    }

    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        super.onServicesDiscovered(gatt, status)
        subscribeProtectedChr(gatt)
    }

    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        when(status) {
            BluetoothGatt.GATT_SUCCESS -> {
                when (newState) {
                    BluetoothProfile.STATE_CONNECTED -> gatt!!.discoverServices()
                    else -> Logger.e("Unknown new state error")
                }
            }
            else -> Logger.i("Unknown status: $newState")
        }
    }
}

// assume `btadapter` is `BluetoothAdapter` type var
btadapter.bondedDevices.forEach { dev: BluetoothDevice ->
    Logger.i(dev.address)
    dev.connectGatt(this, false, gattCallback)
}

既に正規のcentralが正規のperipheralとペアリングを完了していた場合、BluetoothAdapter.bondedDevicesに該当peripheralの情報が入っています。悪意あるコードはdev.connectGatt(...)によって既に確立されたリンクを利用し、gattCallback内で目的のCharacteristicに対して購読を開始しています。これ以降、ドアの開閉状態が変化してperipheralから通知が飛んでくると、その通知をバックグラウンドで動作している悪意あるアプリがキャッチして、その値を読み込むことができるようになってしまいます。

以下が、正規のperipheralと悪意あるアプリが入っているcentralで通信を行う際に通知を盗み見るPoC例です:

(動画) centralと同一端末内の悪意あるデバイスによるnotify sniffing

悪意あるアプリが通知されたドアの開閉状態(01000000 / 00000000)をリークできていることがわかります。

今回は通知される値がドアの開閉状態であるため、そこまでリスクは大きくないと感じるかもしれません。しかし、同じスマートロックシステムでも以下のような場合を想定してみます。

まず、錠前(扉)側は複数のユーザ及び各ユーザに対応する鍵データを、ユーザ登録時に都度生成するものと仮定します。その場合、扉側は生成した鍵をユーザに渡す必要があります。

このperipheralからcentralへの鍵渡しを、notify/indicate機能を用いて実装したとすると、先程PoCで示したとおり悪意あるアプリは通知された鍵をnotifyの値を盗み見ることでリークすることができます。いくらSecureConnectionで認証ありのモデルでペアリングしても、暗号化されるのは経路だけでありnotifyされる鍵の値はそのまま悪用することが可能です。

対策: アプリケーションレイヤにおける通信の暗号化

対策としては、BLEレイヤよりも上のアプリケーションレイヤにおいてデータ通信を暗号化するという方法が挙げられます。

Android内でnotifyがアプリケーションに通知されるまでの経路をBLEリンクレイヤとは別の危険な経路として定義し、ここでの暗号化を施すためにGATTに対して読み書きする秘匿データを全て暗号化すると、例えnotifyで通信内容が露出したとしてもデータは保護されることになります。

この際、暗号化に用いるための鍵は経路上で平文で露出しても良いよう、公開鍵暗号化交換方式(Secure Connectionと同じ方式ですね)等で鍵を交換する必要があります(通信の秘密鍵をアプリケーションにハードコーディングした場合、この暗号化自体が無意味になってしまいます)。

また、ユーザ側もこうした悪意あるアプリが混入しないように注意する必要があります。

なお、GATTへのwriteも同様に正規のアプリが確立した接続を用いてリークできる可能性がありますが、本記事の執筆段階では検証できていません。

まとめ

ここまでで、BLEのペアリングに注目した5つの脆弱性について、その原理や攻撃方法と一緒に紹介してきました。

BLEを用いて通信を行う場合の注意点をまとめます:

  • 秘匿すべき情報のGATT権限は、必ず暗号化必須とする(peripheral端末側)
  • peripheral端末側はSecure Connectionによるペアリングをサポートする(BLE v4.2以降)
  • Just Works以外のモデルを用いてペアリングを行う(SCOモードを適切に使用する):
    • peripheral端末側は入出力機能を持つようにする
    • central側はペアリングに用いたモデルが、要求する以上の堅牢さを持つモデルであるかを確認してから以降の通信を開始する
    • central側は無条件でペアリングを要求する(Androidの場合createBond())
    • デバイススキャン時に複数候補が存在した場合、処理を中止する27
  • (より堅牢な対策をしたい場合には) notify/read等のcharacteristicに平文の機密情報を含めないようにする
    • 機密情報を含めることが必須である場合、アプリケーションレイヤでさらなる暗号化を行う。また、writte時にはperipheral側で書き込み元アプリが正規のものであることを検証する。

文中でも書いたように、上の全てを実装するのは現在のフレームワーク事情により難しい場合も多くあります。またIoTデバイスという性質上、セキュリティ要件的に実装したい内容が実装できないというシチュエーションも多く存在し得ると思います。

その場合でも、システム毎に秘匿すべき情報の優先度を明確に定義してユーザのセキュリティを確保できるような設計とすることが再優先事項となるということには変わりありません。本記事がその一助となれば幸いです。

なお、本記事はBLE core specification・関連論文・AOSPのBluetooth関係のソースコード・Androidデベロッパドキュメント等を基に、実デバイスで実際に検証を行いながら執筆しました。Android若しくはBLEのバージョンによっては本記事の内容と相違点が存在する場合があるかもしれません。また、そもそもに記述内容に誤りが存在するかもしれません28。その場合は、ご教授頂けると幸いです。

最後に、Flatt Securityのセキュリティ診断(脆弱性診断)サービスの紹介です。Flatt Securityでは本記事で題材としたようなスマートロック製品などIoTのセキュリティリスクを専門家が洗い出すサービスを提供しています。

もちろんWebアプリケーションやスマートフォンアプリケーション、AWS・GCP・Azureといったクラウドプラットフォームも含めて包括的な観点も提供可能です。プロダクトごとにぴったりの診断プランを提案いたしますので、是非ご検討ください。

上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。

Flatt Securityはセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式Twitterのフォローをぜひお願いします!

twitter.com

ここまでお読みいただきありがとうございました。

References

  1. Bluetooth Core Specification 4.2 (referenced as CSv4.2 in this blog)
  2. Bluetooth Core Specification 5.2
  3. Bluetooth Low Energyのペアリングとボンディングについて, FIELD DESIGN INC.
  4. Bluetoothのセキュリティのはなし, silex technology, Inc.
  5. Zhang, Y., Weng, J., Dey, R., Jin, Y., Lin, Z., & Fu, X. (2019). On the (in) security of bluetooth low energy one-way secure connections only mode. arXiv preprint arXiv:1908.10497.
  6. Zuo, C., Wen, H., Lin, Z., & Zhang, Y. (2019, November). Automatic fingerprinting of vulnerable ble iot devices with static uuids from mobile apps. In Proceedings of the 2019 ACM SIGSAC Conference on Computer and Communications Security (pp. 1469-1483).
  7. Android Code Search
  8. CrackLE

  1. この図では書かれていないattributeも存在します。 
  2. 4桁のUUIDは0000XXXX-0000-1000-8000-00805F9B34FBの省略版です。
  3. BLEには経路を暗号化するモード(Mode 1)とは別に、データをCSRKという鍵を用いて署名するモード(Mode 2)も用意されていますが、今回はあまり触れません。詳細はCSv4.2 Vol3 PartC page 381等を参照ください。
  4. 当然のことですが、配布プログラム中にハードコーディングした任意の情報は公開情報と見なすべきであり、今回の目的では使うべきではありません。今回はあくまでも通信経路上の秘匿情報の保護に重点を置いているため、端末内の秘匿情報の保護については触れません。
  5. 厳密には、characteristicが暗号化必須でない場合でもペアリングを行ってから通信を行うことが可能です(Androidの場合にはcreateBond()メソッド)。ペアリングの開始タイミングは3種類あり、どのタイミングで実際にペアリングが開始されるかは使用するBLEフレームワークやアプリの実装依存です。
  6. 鍵交換(ペアリング)終了後には、後の通信でも使えるように生成した鍵(LTK)を端末内に保存することができ、これをボンディングと呼びます。
  7. PINコードの長さは6桁の場合が多いですが、BLEの仕様上はもっと長いPINコードを利用することも可能です。
  8. OOBではcentral/peripheral以外に追加モジュールが必要となるため、利便性の低下と導入コストが大きく、あまり利用しているサービスは多くないように思います。
  9. BLEプロトコル・スタックのわかりやすい図表がMathWorksのページにあります。
  10. BLEのGATTプロトコルはリトルエンディアンです(BLE内の他のプロトコルも基本的にはリトルエンディアンですが、一部ビッグエンディアンを利用します)。
  11. BLEでは通信時に利用するデバイスアドレスとして、端末固有のPublic Addressと、可変なRandom Addressの2種類があります。Random Addressは再生成されるタイミング等によってさらに分類されます。詳細はCSv4.2 Vol6 PartB page 33等を参照ください。
  12. Secure Connectionの鍵交換方式については、(普通の公開DH鍵交換方式なので)ここではその詳細に触れません。というか、やっぱり暗号周りは自作なんてするよりも実績のあるものを使うのが一番だとわかりますね...。
  13. 勿論実際のデバイスでどうなっているかはプロトコル・スタックの実装依存ですが、「仕様に従っているならば」そうなります。
  14. 本文にも書いたように、少なくともperipheral側でSCを強制できるように実装されていれば、Android側でその確認ができずともpassiveな盗聴に対する防御となります。脆弱性4で述べるように攻撃者がperipheralを偽って接続した場合には勿論SCを強制することはできなくなってしまいますが、これはpassiveな盗聴とは別問題です。
  15. LinuxのBluetoothプロトコル・スタック実装であるBlueZのソースコード中(/testディレクトリ)にGATTサーバ等のプログラム例があるので、気になる方は参考にしてみてください。
  16. Android7.0以前はこのポップアップすら表示されません。
  17. 両Androidデバイスはネットワークを介して画面をキャプチャしていますが、実際の実デバイス上で動作しています。
  18. 実際に攻撃を行うとなると、ユーザが正規のcentralに対して接続しないようにする必要がありますが、これはperipheralのGATTサーバへのDoS攻撃等によって実現することが可能です。
  19. これはMITMではありません。
  20. 注釈にも書いたように、ペアリングの要求タイミングは実装依存で異なります。本文のようにcharacteristicに暗号必須属性をつけないことでペアリング無しで通信できるのは、Insufficient Encryption/Authenticationをトリガーとしてペアリングを開始する場合のみです。GATTに暗号化必須属性があるかどうかに関わらずペアリングを開始する場合にはやはりペアリングを行う必要があります。
  21. 勿論ユーザが設定画面からマニュアルで消去することは可能です。
  22. 但しその場合でも、一部の情報(IRKやMACアドレス等)はペアリング成功時点で奪取される可能性があるという事は心に留めておく必要があります。
  23. BLEの通信可能範囲はSoC及び環境に依存します。
  24. 個人的には、このようなユーザの注意に依存するセキュリティは崩壊すると思っているので、これは根本的な対策にはならないと思います。(それを言ってしまうと、PINによる認証も結局はユーザ依存なんですが...。NumericComparisonで「端末に表示されたPINと同一であることを確認してください」とか言っても、確認しないエンドユーザがほとんどだと思います...。勿論ユーザのセキュリティ意識・理解を高めるのは大切なことですが、それを生命線にするのはいかがなものです)
  25. 正規peripheralがユーザの最も近い位置に存在するであろうことを想定し、RSSI強度が最も強いデバイスを選択するという方法も考えられますが、これは攻撃者が強い電波強度の偽peripheralを建てることでバイパスされますし、環境依存のため効果は少ないものと考えられます。また、複数候補がある場合にはユーザに選択させるということも考えられますが、ユーザがダイアログ選択時にセキュリティを意識してデバイスを選択することに期待してはいけないため、これもまた効果はあまりないように思えます。
  26. より完全な対策とするためには、脆弱性4で後述するように利用されたペアリングメソッドをcentral側で確認する必要があります。
  27. もちろん、複数候補が存在し得るような製品の場合には中断する必要はありません。スマートロックの場合を考えると、接続候補端末は常に一つであるべきなはずなので、複数候補を検出できた時点で処理を中止することが考えられます。
  28. Bluetooth Core Specificationを一瞥すると分かりますが、PDFはv4.2で2700ページ、v5.2で3200ページあり、人類が読むにはまだ早いようです。