- イントロ
- BLE通信 概観
- 脆弱性 1: Characteristicの権限指定ミスによる平文通信
- 脆弱性 2. Legacy Pairingにおける暗号化された通信のブルートフォース
- 脆弱性 3. Secure ConnectionのJust Worksにおけるperipheralのspoofing
- 脆弱性 4: IO capability spoofingによるdowngrade攻撃
- 脆弱性 5: 正規アプリが確立した接続の悪用
- まとめ
- References
イントロ
こんにちは、株式会社Flatt Securityでインターンをしている@smallkirbyです。
最近はいろいろなものが無線化し、とても便利な世の中になっています。自分自身、無線イヤホンは手放すことができませんし、自宅もDIYでスマートホーム化しようとも考えています。最近では新型コロナへの対策としてスマートフォン及びビーコンを用いて接触情報や混雑状況を共有する仕組みも整備されてきています。世の中が便利になる反面、身近なものの情報化はセキュリティリスクが身近になることとトレードオフの関係にあります。
無線デバイスでよく使われている通信規格がBluetoothです。読者の皆さんの中には、Webで用いられる通信規格・プロトコルについては知っているけど、Bluetoothの仕組みは知らないという方も多いかと思います。本ブログではBluetoothを用いた通信を、ペアリングのセキュリティという観点に注目して掘り下げていこうと思います。
以下では、まずBLE通信で用いられるデータ構造であるGATTや実際の通信におけるペアリングフローについて概観した後に、Bluetoothのペアリングにおいて発生し得る5つの脆弱性について実際のPoCと共に紹介していきます。
なお、Bluetoothは大別してClassic Bluetooth
とBluetooth Low Energy(BLE)
が存在しますが、特に言及しない限り本ブログではBLEについて扱います。
BLE通信 概観
GATTプロファイル
BLE通信ではサーバ・クライアント方式と同様に、端末はperipheral
とcentral
のいずれかの役割を持って通信を開始します。
ここでperipheral
はサーバに相当します(eg: スマートロックの錠前、IoT側)。また、central
はクライアントに相当します(eg: スマートロックの鍵、スマートフォン側)。
BLEにおける通信は、GATT(Generic Attribute Protocol)というデータ構造を軸として行われ、peripheral
は以下に持つようなGATTプロファイルから構成されています:
GATTサーバは1個以上のService
を持っており、Service
は0個以上のCharacteristic
を持っています。このCharacteristic
がperipheral
とcentral
間でやり取りされるデータの一つ一つを表しています。各Characteristic
は0個以上のDescriptor
を持っており、このDescriptor
によってCharacteristic
に更なるメタデータが付与されます。以上のService
・Characteristic
・Descriptor
は全て、attribute
というデータ単位で構成されており、attribute
には権限(permission)を設定することができます。権限には、readable
/writable
などのIOの可否や、経路の暗号化が必須かどうか(encryption)等の情報を設定することができます。
Service
やCharacteristic
は固有の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:
このプロファイルは1つのService
を持ち、このService
は以下の2つのCharacteristic
を持っています:
Key Write Characteristic
: UUIDはDEAD0002-DEAD-DEAD-DEAD-0080DEADBEEF
。central
がドアの鍵を書き込むことでドアを開くことができる。Door Status Characteristic
: UUIDはDEAD0003-DEAD-DEAD-DEAD-0080DEADBEEF
。 ドアの開閉状態を示す。notify
可能。
また、Door Status Characteristic
はCCCD
と呼ばれるUUID: 2902
2の一般的なDescriptor
を持っています。このDescriptor
はnotify
やindicate
を有効・無効にするためのものです。notify
が有効になっている場合、ドアが開閉されてDoor Status Characteristic
の値が変化すると、その変更がcentral
に対して通知されるようになります。
このスマートロックシステムは、central
であるスマホが固定のドアの鍵をperipheral
である錠前(peripheral
)に書き込むことでドアを開閉することのできる単純な構成になっており、以下の図の流れで動作します:
まず、central
はperipheral
に接続するとDoor Status Characteristic
のCCCDに書き込みを行うことで通知を有効にします。その後、Key Write Characteristic
に対して固定のドアの鍵を書き込みます。peripheral
は書き込まれた鍵が正規のドアの鍵であることを確認した後、正しければドアを開きます。続いてperipheral
はDoor Status Characteristic
の値を変更し、変更をcentral
に対して通知します。
以降の検証では、このスマートロックシステムにおけるperipheral
とcentral
の両方をAndroidアプリとして実装したサンプルアプリを使います。実際のシステムではperipheral
はドアにつけるIoTデバイスとして実装されます。このサンプルアプリは、以下の動画のように動作します(動画):
ペアリング
GATT通信自体は、ペアリングを行わずとも実行することができます。Androidの場合には、BluetoothDevice.connectGatt()
を呼ぶことでペアリングを行わずにGATT通信を開始することができます。しかし、この状態で読み書きができるCharacteristic
は暗号化(及び認証)が必要とされていないCharacteristic
のみに限られます。
通信経路を暗号化するためには、ペアリング3というフローを踏む必要があります。ペアリングとは、peripheral
とcentral
の間で鍵を生成・交換する処理のことを指し、その鍵を用いて以降の通信を行います。詳細については、後ほど解説します。
脆弱性 1: Characteristic
の権限指定ミスによる平文通信
観点: GATT Characteristic
と属性
以下では、BLEペアリングに関して発生し得る脆弱性を紹介していきます。
今回用いるスマートロックシステムにおいて、通信経路内の保護すべき情報はKey Write Characteristic
に書き込まれるドアの秘密鍵の値です(今回の実装ではこの値はperipheral
及びcentral
に0xDEADBEEFCAFEBABE
という値でハードコーティングされています4)。
Androidでは、以下のようにしてcharacteristic
の生成及び権限の指定をすることができます。今回peripheral
側では以下のようにKey Write Characteristic
を定義しています(以降の章では都度この宣言を変更していきます):
private val chrKeyVal = BluetoothGattCharacteristic( UUID.fromString(chrKeyValUUID), BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE)
この宣言では、権限をPERMISSION_WRITE
としています。これはPERMISSION_WRITE_ENCRYPTED
やPERMISSION_WRITE_ENCRYPTED_MITM
と異なり、このcharacteristic
を読み書きするのに暗号化(ペアリング)が必要ないことを示しています5。つまり、central
がperipheral
に対して秘密鍵を送信する際、秘密鍵が平文の状態で送信されます。
みなさんもご存知のとおり、Bluetoothは電波に情報を載せて通信するため、受信範囲内の全てのデバイスは通信を傍受することが可能です。よって、上の例のようにcharacteristic
に適切な権限を付与しないまま通信してしまうと、容易に秘密鍵が盗聴されてしまいます。
実際にこの場合の通信を観測してみると以下のように、扉を開けるためにcentral
からperipheral
に対して送信された秘密鍵が露出していることが分かります(ドアの秘密鍵は0xDEADBEEFCAFEBABE
):
ドアの秘密鍵を盗聴してリークした攻撃者は、central
と偽ってこの鍵を送信することで任意のタイミングであなたの家のドアを開けることができてしまいます。
対策: characteristic
への暗号化必須属性の付与
このケースにおける問題点は明らかです。Bluetooth通信は誰でも傍受することができます。
よって、秘匿する必要のあるデータ(ここではドアの秘密鍵)を送受信する場合には、例外なくcharacteristicに暗号化必須属性を指定する必要があります。
脆弱性 2. Legacy Pairingにおける暗号化された通信のブルートフォース
前節では通信経路上に秘匿情報が載せられる場合、characteristic
にはencryption required
(暗号化必須)権限を指定しなければならないことを確認しました。これは、通信の際に必ずペアリングをするということと同義です。
ペアリングとは、peripheral
とcentral
の間で鍵を交換し、その鍵を用いて通信経路を暗号化することを指します6。
さて、ペアリング方式にはAssociation Model
と呼ばれる4つの方法があります:
Numeric Comparison
: 両端末に共通の6つの数字を表示し、同一であることを確認させる(後述するSecure Connection
のみ)Just Works
:Numeric Comparison
において固定の数字を利用し、両端末にはその数字を表示しないPasskey Entry
: 片方の端末で6つの数字(PIN)7を表示し、もう片方の端末でその数字を入力させるOut of Band(OOB)
: BLEと関係のない経路で鍵を渡す
みなさんが頻繁に使うのは、OOB
8を除く3つだと思います。 このうちJust Works
はペアリングすることを告げるダイアログを除いてユーザインタラクションを必要としないため、ユーザがペアリングしていることを意識する必要があるのはPasskey Entry
とNumeric Comaparison
の2つです。
Passkey Entry
は、以下のように片方の端末に表示されたコードをもう片方の端末に入力することでペアリングを行う方式です:
では経路を暗号化するためにKey Write Characteristic
の権限を暗号化必須(Androidの場合PERMISSION_WRITE_ENCRYPTED
)として通信を行ってみます。
以下はペアリングを開始した直後にperipheral
とcentral
の間でやり取りされたパケットリストの一部です:
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
の仕組みとLegacy Pairing
が持っている脆弱性について確認していきます。
LE Legacy Pairingにおける鍵生成と鍵交換
(クリックすると
Legacy Pairing
におけるSTK
生成までのフローを説明します。)
Legacy Pairing
では、鍵生成及び交換がBLE独自の交換方式に則って行われます。Legacy Pairing
のPasskey Entry
における経路暗号化がどのようなフローで行われるかを順に追ってみましょう。
なお、暗号通信の開始要求を送る側をMaster、答える側をSlaveと呼ぶことがありますが、以下では前者をイニシエータ
、後者を非イニシエータ
と呼ぶこととします。
TK
の生成
最初に経路を暗号化するために使われる128bitの鍵のことをSTK
と呼びます。このSTK
はperipheral
とcentral
で共通です。STK
はTK
という128bitのやはりperipheral
とcentral
で共通の値をパラメタとして生成されます。
TK
の交換方法はAssociation Model
に応じて異なります。Passkey Entry
の場合には、片方の端末に表示された6桁の数字を、ユーザがもう片方の端末に入力することで通信経路上にTKが載ることなくTKを共有することができます。
TK
は、Passkey Entry
の場合Passkeyを単純に128bitに拡張した値になります。例えばPINが884933
であった場合、これを16進数128bitに拡張した0x000000000000000000000000000D80C5
がTK
となります。
random値の生成
Passkey Entry
では、ユーザ(central
)がperipheral
を認証するための仕組みを提供します。ユーザはperipheral
とcentral
に対して同じ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
: 3dc393920b9d2cfbea70ad130490a032Sconfirm
: 3cc8c0379cf6a7856d46b475c6787efcMrandom
: d7690095e642c2666066089f67cbe376Srandom
: 1a415e992bae10ecf3447d11851aa4b2preq
: 0f0f102d000401 (ペアリングリクエストのパケット本体)pres
: 05071005000102 (ペアリングレスポンスのパケット本体)ia
: 7d68d5e3c0c3 (イニシエータアドレス)ra
: 44e5171465f3 (非イニシエータアドレス)iat
: 1 (イニシエータアドレスタイプ)11rat
: 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
は0xD80C5 == 633624
であり、ユーザが入力したPINと一致します。
このようにLegacy Pairing
においては、Passkey Entry
・Just 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
の総当りと通信の復号実践
上記のサンプルコードでTK
を総当りで求めることができました。TK
さえ求めれば、STK
は専用の関数に入れるだけで瞬時に求まり、そのSTK
を用いて経路を復号することでLTK
をリークし全ての通信を復号することができてしまいます。
TK
の総当りからpcapファイルの復号までを自動で行ってくれるライブラリとして、CrackLE
というものがあります。CrackLEを用いて先程の暗号化されたpcapファイルを復号すると、以下のようになります:
ブルートフォースによってTKを計算し、そこからさらに経路の暗号化に使われるLTKを算出できていることがわかります。このLTKを用いて経路を復号することで、攻撃者はやはり秘密鍵を入手して自由にドアを開けられるようになりました。
対策: Legacy vs Secure Connection
さて12、ここで問題となっていたのはLegacy Pairing
を使うことで例えペアリングをしていたとしても攻撃者が通信を復号できてしまうことでした(このような盗聴のことを、passiveな盗聴と呼びます)。
解決方法としては単純で、Secure Connection
を使えばいいということになります。
但し、Secure Connection
をサポートしていない(Bluetooth v4.2以前)端末と通信をする場合には当然Secure Connection
を使うことはできません。この場合は、次善の策として後述の脆弱性 5の対策にもあるように、アプリケーションレイヤに独自の暗号化を入れる等の対策が考えられます。
ここで、Legacy Pairing
とSecure Connection
のどちらが使われるかは、何によって決められるのでしょうか。
これはBLEの仕様で決められており、Pairing Request
及びPairing Response
で交換される情報の中でperipheral
とcentral
がSecure 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 Model
とJust 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 Comaprison
とPasskey Entry
ではユーザが端末を認証することが可能となっています。しかしながら、Just Works
においては認証のためのユーザインタラクションがないため、ユーザ(central
)が接続しようとしているperipheral
を正規のものかどうか認証することができません。
これが3つ目の問題点となります。
観点: Just Works
における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
がドアをサーチした後、攻撃者(画面右ターミナル)に対して接続し、ペアリングを行っていることがわかります。ペアリングは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 Comparison
かPasskey 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等)
たとえばKeyboardOnly
のperipheral
とKeyboardDisplay
のcentral
がペアリングしようとしたとします(これは、HHKB等の無線キーボードとPCを接続しようとする場合に当てはまります)。
この場合、PasskeyEntry
を選択してPCに出力されたPINをキーボードに打ち込むということは可能ですが、両デバイスにPINを表示する必要のあるNumeric Comaprison
は利用することができません。このように、両端末のIO capability
に応じてペアリングで使用するモデルがマッピングされています(CS v4.2 vol3 partH page 610-611より引用):
上のマップからも分かるとおり、ペアリングモデルはIO capabilityの劣っている方に合わせて選択されることになります。そして、この交渉はペアリングを開始するPairing Request / Pairing Response
のパケット中で相手に対して通知されます:
本システムのPairing Request
を送る側(central
)はAndroid端末であるため入出力機能をともに有しており、上の例では確かにIO Capability
としてKeyboard, Display (0x04)
が通知されています。
よって、Just Works
で偽のperipheral
に接続される問題に対しては、peripheralにPasskey Entry
かNumeric Comaprison
を行うための入出力機能を実装し、ペアリングリクエスト時に自身のIO capabilityを相手に通知するということが対策となります。
脆弱性 4: IO capability spoofingによるdowngrade攻撃
観点: IO capability
のspoofing
さて、Characteristic
に暗号化必須の属性をつけ、Secure Connection
を使い、さらにperipheral
にはちゃんとディスプレイかキーインプットをつけてJustWorks
以外で認証もちゃんと行うようにしたとしましょう。これでもう、盗聴もされないしperipheral
を詐称される恐れもなくてもう安全!... と思いきや、まだこの実装には脆弱性があります。ここで残っているのは脆弱性 3と同じ、peripheral
の詐称(spoofing)です。
HTTPS通信におけるダウングレード攻撃を聞いたことがあるでしょうか。TLSにおけるダウングレード攻撃は、通信開始の際に中間者が通信に介在して通信を改ざんすることで弱い暗号スイートの使用を強制することを指します。そして、BLE通信においてもこれと似たようなダウングレード攻撃が成立します。
以下は、攻撃者がやはりperipheral
を偽ったGATTサーバを構築し、central
を接続させようとしている場合を想定します19。
先程、ペアリングで使用するAssociation Model
は通信の開始時におけるIO capability
の交換によって決定されることを確認しました。ここで、攻撃者が敢えて低いIO capability
でcentral
に応答したとします。
例えば自身のIO capability
をNoInputNoOutput
と偽ってPairing Response
を返したとすると、上のマッピングより利用されるモデルはJust Works
になってしまいます。先程確認したとおりJust Works
は端末の認証ができないためユーザはやはり接続先が攻撃者のサーバであることに気がつかずに接続して秘密鍵を書き込んでしまうことになります。
ここで正規のperipheral
は一切関与していないため、いくら正規のperipheral
が入出力機能を有していたとしても関係なくJust Works
が使われることになってしまいます。
そして、攻撃者はこのIO capability
を任意の値に指定することができます。Linuxにおいては、利用するIO capability
をbtmgmt
(やbluetoothctl
)等のコマンド(dbusが提供するAPIのラッパー)を利用して変更できます。
先程お見せした動画では以下のスクリプトを用いてIO capability
をNoInputNoOutput
にし、通信に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つの手かと思います23。24
追加の緩和策として、peripheral
のデバイススキャン時に複数候補が見つかった場合にはエラーを表示するという方法も考えられます25。しかしながら、攻撃者が正規のperipheral
に対して延々とペアリング要求を贈り続ける(且つ、peripheral
がペアリング開始時に広告を停止する実装になっている)ような場合には、ユーザが検索可能なデバイスは攻撃者のperipheral
のみになってしまいます(DoS攻撃)。
よって、やはりスキャンに依存する対策だけでは不十分であり、あくまでも先述の方法で対策を行った上での追加策と考えるべきかもしれません。
また、central
側の対策として権限不足によるperipheral
からのエラーをトリガーとしてペアリングを開始するのではなく、central
側から無条件でペアリング要求を行うことが考えられます。暗号化必須でないcharacteristic
に対してもcentral
側からペアリングを行うことで、攻撃者が偽のperipheral
で暗号化無しで通信を行おうとしても必ずペアリングを行うことができます。これによって、characteristic
を暗号化必須でない権限に設定にすることで平文通信を行わせるspoofing対する対策となります26。Androidの場合にはcreateBond()
メソッドによって実現可能です。
脆弱性 5: 正規アプリが確立した接続の悪用
観点: 同一Android端末におけるnotify通知
さて、最後にペアリングからは少し違う観点の脆弱性を一つ紹介します。
先程から少し登場していますが、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例です:
悪意あるアプリが通知されたドアの開閉状態(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のフォローをぜひお願いします!
ここまでお読みいただきありがとうございました。
References
- Bluetooth Core Specification 4.2 (referenced as
CSv4.2
in this blog) - Bluetooth Core Specification 5.2
- Bluetooth Low Energyのペアリングとボンディングについて, FIELD DESIGN INC.
- Bluetoothのセキュリティのはなし, silex technology, Inc.
- 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.
- 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).
- Android Code Search
- CrackLE
-
この図では書かれていない
attribute
も存在します。 ↩ -
4桁のUUIDは
0000XXXX-0000-1000-8000-00805F9B34FB
の省略版です。↩ -
BLEには経路を暗号化するモード(
Mode 1
)とは別に、データをCSRK
という鍵を用いて署名するモード(Mode 2
)も用意されていますが、今回はあまり触れません。詳細はCSv4.2 Vol3 PartC page 381等を参照ください。↩ - 当然のことですが、配布プログラム中にハードコーディングした任意の情報は公開情報と見なすべきであり、今回の目的では使うべきではありません。今回はあくまでも通信経路上の秘匿情報の保護に重点を置いているため、端末内の秘匿情報の保護については触れません。↩
-
厳密には、
characteristic
が暗号化必須でない場合でもペアリングを行ってから通信を行うことが可能です(Androidの場合にはcreateBond()
メソッド)。ペアリングの開始タイミングは3種類あり、どのタイミングで実際にペアリングが開始されるかは使用するBLEフレームワークやアプリの実装依存です。↩ -
鍵交換(ペアリング)終了後には、後の通信でも使えるように生成した鍵(
LTK
)を端末内に保存することができ、これをボンディングと呼びます。↩ - PINコードの長さは6桁の場合が多いですが、BLEの仕様上はもっと長いPINコードを利用することも可能です。↩
-
OOBでは
central
/peripheral
以外に追加モジュールが必要となるため、利便性の低下と導入コストが大きく、あまり利用しているサービスは多くないように思います。↩ - BLEプロトコル・スタックのわかりやすい図表がMathWorksのページにあります。↩
- BLEのGATTプロトコルはリトルエンディアンです(BLE内の他のプロトコルも基本的にはリトルエンディアンですが、一部ビッグエンディアンを利用します)。↩
-
BLEでは通信時に利用するデバイスアドレスとして、端末固有の
Public Address
と、可変なRandom Address
の2種類があります。Random Address
は再生成されるタイミング等によってさらに分類されます。詳細はCSv4.2 Vol6 PartB page 33等を参照ください。↩ -
Secure Connection
の鍵交換方式については、(普通の公開DH鍵交換方式なので)ここではその詳細に触れません。というか、やっぱり暗号周りは自作なんてするよりも実績のあるものを使うのが一番だとわかりますね...。↩ - 勿論実際のデバイスでどうなっているかはプロトコル・スタックの実装依存ですが、「仕様に従っているならば」そうなります。↩
-
本文にも書いたように、少なくとも
peripheral
側でSCを強制できるように実装されていれば、Android側でその確認ができずともpassiveな盗聴に対する防御となります。脆弱性4で述べるように攻撃者がperipheral
を偽って接続した場合には勿論SCを強制することはできなくなってしまいますが、これはpassiveな盗聴とは別問題です。↩ -
LinuxのBluetoothプロトコル・スタック実装であるBlueZのソースコード中(
/testディレクトリ
)にGATTサーバ等のプログラム例があるので、気になる方は参考にしてみてください。↩ - Android7.0以前はこのポップアップすら表示されません。↩
- 両Androidデバイスはネットワークを介して画面をキャプチャしていますが、実際の実デバイス上で動作しています。↩
-
実際に攻撃を行うとなると、ユーザが正規の
central
に対して接続しないようにする必要がありますが、これはperipheral
のGATTサーバへのDoS攻撃等によって実現することが可能です。↩ - これはMITMではありません。↩
-
注釈にも書いたように、ペアリングの要求タイミングは実装依存で異なります。本文のように
characteristic
に暗号必須属性をつけないことでペアリング無しで通信できるのは、Insufficient Encryption/Authentication
をトリガーとしてペアリングを開始する場合のみです。GATTに暗号化必須属性があるかどうかに関わらずペアリングを開始する場合にはやはりペアリングを行う必要があります。↩ - 勿論ユーザが設定画面からマニュアルで消去することは可能です。↩
-
但しその場合でも、一部の情報(
IRK
やMACアドレス等)はペアリング成功時点で奪取される可能性があるという事は心に留めておく必要があります。↩ - BLEの通信可能範囲はSoC及び環境に依存します。↩
-
個人的には、このようなユーザの注意に依存するセキュリティは崩壊すると思っているので、これは根本的な対策にはならないと思います。(それを言ってしまうと、PINによる認証も結局はユーザ依存なんですが...。
NumericComparison
で「端末に表示されたPINと同一であることを確認してください」とか言っても、確認しないエンドユーザがほとんどだと思います...。勿論ユーザのセキュリティ意識・理解を高めるのは大切なことですが、それを生命線にするのはいかがなものです)↩ -
正規
peripheral
がユーザの最も近い位置に存在するであろうことを想定し、RSSI強度が最も強いデバイスを選択するという方法も考えられますが、これは攻撃者が強い電波強度の偽peripheral
を建てることでバイパスされますし、環境依存のため効果は少ないものと考えられます。また、複数候補がある場合にはユーザに選択させるということも考えられますが、ユーザがダイアログ選択時にセキュリティを意識してデバイスを選択することに期待してはいけないため、これもまた効果はあまりないように思えます。↩ -
より完全な対策とするためには、脆弱性4で後述するように利用されたペアリングメソッドを
central
側で確認する必要があります。↩ - もちろん、複数候補が存在し得るような製品の場合には中断する必要はありません。スマートロックの場合を考えると、接続候補端末は常に一つであるべきなはずなので、複数候補を検出できた時点で処理を中止することが考えられます。↩
- Bluetooth Core Specificationを一瞥すると分かりますが、PDFはv4.2で2700ページ、v5.2で3200ページあり、人類が読むにはまだ早いようです。↩