作者:huahuaisadog@360VulpeckerTeam
來源:安全客
0x00
前些天,armis爆出了一系列藍牙的漏洞,無接觸無感知接管系統的能力有點可怕,而且基本上影響所有的藍牙設備,危害不可估量,可以看這里來了解一下它的逆天能力:只要手機開啟了藍牙,就可能被遠程控制。現在手機這么多,利用這個漏洞寫出蠕蟲化的工具,那么可能又是一個手機版的低配wannacry了。我們360Vulpecker Team在了解到這些相關信息后,快速進行了跟進分析。 armis給出了他們的whitepaper,對藍牙架構和這幾個漏洞的分析可以說非常詳盡了,先膜一發。不過他們沒有給出這些漏洞的PoC或者是exp,只給了一個針對Android的“BlueBorne檢測app",但是逆向這個發現僅僅是檢測了系統的補丁日期。于是我來拾一波牙慧,把這幾個漏洞再分析一下,然后把poc編寫出來:
-
CVE-2017-1000250 Linux bluetoothd進程信息泄露
-
CVE-2017-1000251 Linux 內核棧溢出
-
CVE-2017-0785 Android com.android.bluetooth進程信息泄露
-
CVE-2017-0781 Android com.android.bluetooth進程堆溢出
-
CVE-2017-0782 Android com.android.bluetooth進程堆溢出
以上PoC代碼均在https://github.com/marsyy/littl_tools/tree/master/bluetooth
由于也是因為這幾個漏洞才從零開始搞藍牙,所以應該有些分析不到位的地方,還請各路大牛斧正。
0x01 藍牙架構及代碼分布
這里首先應該祭出armis的paper里的圖:

圖上把藍牙的各個層次關系描述得很詳盡,不過我們這里暫時只需要關心這么幾層:HCI,L2CAP,BNEP,SDP。BNEP和SDP是比較上層的服務,HCI在最底層,直接和藍牙設備打交道。而承載在藍牙服務和底層設備之間的橋梁,也就是L2CAP層了。每一層都有它協議規定的數據組織結構,所有層的數據包組合在一起,就是一個完整的藍牙包(一個SDP包為例):

雖然協議規定的架構是圖上說的那樣,但是具體實現是有不同的,Linux用的BlueZ,而現在的Android用的BlueDroid,也就針對這兩種架構說一說代碼的具體分布。
BlueZ
在Linux里,用的是BlueZ架構,由bluetoothd來提供BNEP,SDP這些比較上層的服務,而L2CAP層則是放在內核里面。對于BlueZ我們對SDP和L2CAP挨個分析。
1, 實現SDP服務的代碼在代碼目錄的/src/sdp,其中sdp-client.c是它的客戶端,sdp-server.c是它的服務端。我們要分析的漏洞都是遠程的漏洞,所以問題是出在服務端里面,我們重點關注服務端。而服務端最核心的代碼,應該是它對接受到的數據包的處理的過程,這個過程由sdp-request.c來實現。當L2CAP層有SDP數據后,會觸發sdp-server.c的io_session_event函數,來獲取這個數據包,交由sdp-request.c的handle_request函數處理(怎么處理的,后續漏洞分析的時候再講):
static gboolean io_session_event(GIOChannel *chan, GIOCondition cond, gpointer data)
{
...
len = recv(sk, &hdr, sizeof(sdp_pdu_hdr_t), MSG_PEEK); //獲取SDP的頭部數據,獲得SDP數據大小
if (len < 0 || (unsigned int) len < sizeof(sdp_pdu_hdr_t)) {
sdp_svcdb_collect_all(sk);
return FALSE;
}
size = sizeof(sdp_pdu_hdr_t) + ntohs(hdr.plen);
buf = malloc(size);
if (!buf)
return TRUE;
len = recv(sk, buf, size, 0); //獲得完整數據包
...
handle_request(sk, buf, len);
return TRUE;
}
2, L2CAP層的代碼在內核里,這里我以Linux 4.2.8這份代碼為例。l2cap層主要由 /net/bluetooth/l2capcore.c和/net/bluetooth/l2capsock.c來實現。l2capcore.c實現了L2CAP協議的主要內容,l2capsock.c 通過注冊sock協議的方式提供了這一層針對userspace的接口。同樣的我們關心一個L2CAP對接受到數據包后的處理過程,L2CAP的數據是由HCI層傳過來的,在hcicore.c的hcirxwork函數里
static void hci_rx_work(struct work_struct *work)
{
while ((skb = skb_dequeue(&hdev->rx_q))) {
/* Send copy to monitor */
hci_send_to_monitor(hdev, skb);
...
switch (bt_cb(skb)->pkt_type) {
case HCI_EVENT_PKT:
BT_DBG("%s Event packet", hdev->name);
hci_event_packet(hdev, skb);
break;
case HCI_ACLDATA_PKT:
BT_DBG("%s ACL data packet", hdev->name);
hci_acldata_packet(hdev, skb);
break;
case HCI_SCODATA_PKT:
BT_DBG("%s SCO data packet", hdev->name);
hci_scodata_packet(hdev, skb);
break;
default:
kfree_skb(skb);
break;
}
}
}
收到數據后,會判斷pkt_type,符合L2CAP層的type是HCI_ACLDATA_PKT,函數會走到hci_acldata_packet,這個函數會把HCI的數據剝離之后,把L2CAP數據交給L2CAP層的l2cap_recv_acldata:
static void hci_acldata_packet(struct hci_dev *hdev, struct sk_buff *skb)
{
...
skb_pull(skb, HCI_ACL_HDR_SIZE);
...
if (conn) {
hci_conn_enter_active_mode(conn, BT_POWER_FORCE_ACTIVE_OFF);
/* Send to upper protocol */
l2cap_recv_acldata(conn, skb, flags);
return;
} else {
BT_ERR("%s ACL packet for unknown connection handle %d",
hdev->name, handle);
}
kfree_skb(skb);
}
同樣的,對于L2CAP層對數據的細致處理,我們還是等后續和漏洞來一塊進行分析。
BlueDroid
在現在的Android里,用的是BlueDroid架構。這個和BlueZ架構有很大不同的一點是:BlueDroid將L2CAP層放在了userspace。SDP,BNEP,L2CAP統統都由com.android.bluetooth這個進程管理。而BlueDroid代碼的核心目錄在Android源碼目錄下的 /sytem/bt ,這個目錄的核心產物是bluetooth.default.so,這個so集成所有Android藍牙相關的服務,而且這個so沒有導出任何相關接口函數,只導出了幾個協議相關的全局變量供使用,所以想根據so來本地檢測本機是否有BlueDrone漏洞,是一件比較困難的事情。對于BlueDroid,由于android的幾個漏洞出在BNEP服務和SDP服務,所以也就主要就針對這兩塊。值得注意的是,在Android里,不論是64位還是32位的系統,這個bluetooth.default.so都是用的32位的。文章里這部分代碼都基于Android7.1.2的源碼。
1,BlueDroid的SDP服務的代碼,在/system/bt/stack/sdp 文件夾里,其中sdp服務端對數據包的處理由sdp-server.c實現。SDP連接建立起來后,在收到SDP數據包之后呢,會觸發回調函數sdpdataind,這個函數會把數據包交個sdp-server.c的sdpserverhandleclientreq函數進行處理:
static void sdp_data_ind (UINT16 l2cap_cid, BT_HDR *p_msg)
{
tCONN_CB *p_ccb;
if ((p_ccb = sdpu_find_ccb_by_cid (l2cap_cid)) != NULL)
{
if (p_ccb->con_state == SDP_STATE_CONNECTED)
{
if (p_ccb->con_flags & SDP_FLAGS_IS_ORIG)
sdp_disc_server_rsp (p_ccb, p_msg);
else
sdp_server_handle_client_req (p_ccb, p_msg);
}
...
}
2,BlueDroid的BNEP服務的代碼主要在/system/bt/stack/bnep/bnepmain.c。BNEP連接建立起來后,再收到BNEP的包,和SDP類似,會觸發回調函數bnepdata_ind,這個函數包含了所有對BNEP請求的處理,漏洞也是發生在這里,具體的代碼我們后續會分析。
0x02 漏洞分析以及PoC寫法
藍牙的預備知識差不多了,主要是找數據包的入口。我們再基于漏洞和PoC的編寫過程來詳細分析其中的處理過程,和相關藍牙操作的代碼該怎么寫。
CVE-2017-1000251
這個是Linux L2CAP層的漏洞,那么就是內核里面的。先不著急看漏洞,先看L2CAP層如何工作。在一個L2CAP連接的過程中,我們抓取了它的數據包來分析,L2CAP是怎么建立起連接的:

我們注意這么幾個包: sentinfomationrequest , sendconnectionrequest, sendconfigurerequest。抓包可以看到,在一次完整的L2CAP連接的建立過程中,發起連接的機器,會主動送出這么幾個包。其中infomationrequest是為了得到對方機器的名稱等信息,connectionrequest是為了建立L2CAP真正的連接,主要是為了確定雙方的CHANNEL ID,后續的數據包傳輸都要跟著這個channel id 走(圖上的SCID, DCID),這個channel也就是我們所說的連接。在connectionrequest處理完畢之后,連接狀態將變成 BTCONNECT2 。隨后機器會發起configure_request,這一步就到了armis的paper第十頁所說的configuration process:

這個過程完成后,整個L2CAP層的連接也就建立完成。
從上述過程看,可以發現L2CAP層連接的建立,主要是對上述三個請求的發起和處理。而我們的漏洞,也其實就發生在configuration process。我們先分析接收端收到這三個請求后,處理的邏輯在哪里,也就是我們前文提到的L2CAP對接受到的數據的處理過程:
1,在l2caprecvacldata接收到數據后,數據包會傳給l2cap_recvframe
2,l2caprecvframe會取出檢查L2CAP的頭部數據,然后檢查根據頭部里的cid字段,來選擇處理邏輯:
static void l2cap_recv_frame(struct l2cap_conn *conn, struct sk_buff *skb)
{
...
skb_pull(skb, L2CAP_HDR_SIZE);
cid = __le16_to_cpu(lh->cid);
len = __le16_to_cpu(lh->len);
switch (cid) {
case L2CAP_CID_SIGNALING:
l2cap_sig_channel(conn, skb);
break;
case L2CAP_CID_CONN_LESS:
psm = get_unaligned((__le16 *) skb->data);
skb_pull(skb, L2CAP_PSMLEN_SIZE);
l2cap_conless_channel(conn, psm, skb);
break;
case L2CAP_CID_LE_SIGNALING:
l2cap_le_sig_channel(conn, skb);
break;
default:
l2cap_data_channel(conn, cid, skb);
break;
}
3,底層L2CAP的連接,cid固定是L2CAP_CID_SIGNALING,于是會走l2cap_sig_channel,l2cap_sig_channel得到的是剝離了頭部的L2CAP的數據,這一部將把數據里的cmd頭部解析并剝離,再傳給l2cap_bredr_sig_cmd進行處理:
static inline void l2cap_sig_channel(struct l2cap_conn *conn,
struct sk_buff *skb)
{
...
while (len >= L2CAP_CMD_HDR_SIZE) {
u16 cmd_len;
memcpy(&cmd, data, L2CAP_CMD_HDR_SIZE); //取得cmd頭部數據
data += L2CAP_CMD_HDR_SIZE;
len -= L2CAP_CMD_HDR_SIZE;
cmd_len = le16_to_cpu(cmd.len); //取得cmd的大小
...
err = l2cap_bredr_sig_cmd(conn, &cmd, cmd_len, data); //傳給l2cap_bredr_sig_cmd處理
...
data += cmd_len;
len -= cmd_len;
}
drop:
kfree_skb(skb);
}
到這里,我們應該能得出L2CAP協議的數據結構:

4, 隨后數據進入到了l2cap_bredr_sig_cmd函數進行處理。這里也就是處理L2CAP各種請求的核心函數了:
static inline int l2cap_bredr_sig_cmd(struct l2cap_conn *conn,
struct l2cap_cmd_hdr *cmd, u16 cmd_len,
u8 *data)
{
int err = 0;
switch (cmd->code) {
case L2CAP_CONN_REQ:
err = l2cap_connect_req(conn, cmd, cmd_len, data);
break;
case L2CAP_CONN_RSP:
case L2CAP_CREATE_CHAN_RSP:
l2cap_connect_create_rsp(conn, cmd, cmd_len, data);
break;
case L2CAP_CONF_REQ:
err = l2cap_config_req(conn, cmd, cmd_len, data);
break;
case L2CAP_CONF_RSP:
l2cap_config_rsp(conn, cmd, cmd_len, data); //漏洞函數
break;
...
case L2CAP_INFO_REQ:
err = l2cap_information_req(conn, cmd, cmd_len, data);
break;
case L2CAP_INFO_RSP:
l2cap_information_rsp(conn, cmd, cmd_len, data);
break;
...
}
return err;
}
好了,接下來終于可以分析漏洞了。我們的漏洞發生在對L2CAP_CONFIGRSP(config response)這個cmd的處理上。其實漏洞分析armis的paper已經寫的很詳盡了,我這里也就權當翻譯了吧,然后再加點自己的理解。那么來看l2capconfigrsp:
static inline int l2cap_config_rsp(struct l2cap_conn *conn,
struct l2cap_cmd_hdr *cmd, u16 cmd_len,
u8 *data)
{
struct l2cap_conf_rsp *rsp = (struct l2cap_conf_rsp *)data;
...
scid = __le16_to_cpu(rsp->scid); //從包中剝離出scid
flags = __le16_to_cpu(rsp->flags); //從包中剝離出flag
result = __le16_to_cpu(rsp->result); //從包中剝離出result
switch (result) {
case L2CAP_CONF_SUCCESS:
l2cap_conf_rfc_get(chan, rsp->data, len);
clear_bit(CONF_REM_CONF_PEND, &chan->conf_state);
break;
case L2CAP_CONF_PENDING:
set_bit(CONF_REM_CONF_PEND, &chan->conf_state);
if (test_bit(CONF_LOC_CONF_PEND, &chan->conf_state)) { //判斷conf_state是否是CONF_LOC_CONF_PEND
char buf[64]; //buf數組大小64字節
len = l2cap_parse_conf_rsp(chan, rsp->data, len,
buf, &result); //data仍然是包中數據,len也是包中數據。
...
}
goto done;
...
當收到的數據包里,滿足result == L2CAP_CONF_PENDING,且自身的連接狀態conf_state == CONF_LOC_CONF_PEND的時候,會走到 l2cap_parse_conf_rsp函數里,而且傳過去的buf是個長度為64的數據,參數len ,參數rsp->data都是由包中的內容來任意確定。那么在l2cap_parse_conf_rsp函數里:
static int l2cap_parse_conf_rsp(struct l2cap_chan *chan, void *rsp, int len,
void *data, u16 *result)
{
struct l2cap_conf_req *req = data;
void *ptr = req->data;
int type, olen;
unsigned long val;
while (len >= L2CAP_CONF_OPT_SIZE) { //len沒有被檢查,由接收到的包內容控制
len -= l2cap_get_conf_opt(&rsp, &type, &olen, &val);
switch (type) {
case L2CAP_CONF_MTU:
if (val < L2CAP_DEFAULT_MIN_MTU) {
*result = L2CAP_CONF_UNACCEPT;
chan->imtu = L2CAP_DEFAULT_MIN_MTU;
} else
chan->imtu = val;
l2cap_add_conf_opt(&ptr, L2CAP_CONF_MTU, 2, chan->imtu);
break;
case ...
}
}
}
static void l2cap_add_conf_opt(void **ptr, u8 type, u8 len, unsigned long val)
{
struct l2cap_conf_opt *opt = *ptr;
opt->type = type;
opt->len = len;
switch (len) {
case 1:
*((u8 *) opt->val) = val;
break;
case 2:
put_unaligned_le16(val, opt->val);
break;
case 4:
put_unaligned_le32(val, opt->val);
break;
default:
memcpy(opt->val, (void *) val, len);
break;
}
*ptr += L2CAP_CONF_OPT_SIZE + len;
}
仔細閱讀這個函數的代碼可以知道,這個函數的功能就是根據傳進來的包,來構造將要發出去的包。而數據的出口就是傳進去的64字節大小的buf。但是對傳入的包的數據的長度并沒有做檢驗,那么當len很大時,就會一直往出口buf里寫數據,比如有64個L2CAP_CONF_MTU類型的opt,那么就會往buf里寫上64*(L2CAP_CONF_OPT_SIZE + 2)個字節,那么顯然這里就發生了溢出。由于buf是棧上定義的數據結構,那么這里就是一個棧溢出。 不過值得注意的是,代碼要走進去,需要conf_state == CONF_LOC_CONF_PEND,這個狀態是在處理L2CAP_CONF_REQ數據包的時候設置的:
static int l2cap_parse_conf_req(struct l2cap_chan *chan, void *data)
{
...
u8 remote_efs = 0;
u16 result = L2CAP_CONF_SUCCESS;
...
while (len >= L2CAP_CONF_OPT_SIZE) {
len -= l2cap_get_conf_opt(&req, &type, &olen, &val);
hint = type & L2CAP_CONF_HINT;
type &= L2CAP_CONF_MASK;
switch (type) {
...
case L2CAP_CONF_EFS:
remote_efs = 1; //【1】
if (olen == sizeof(efs))
memcpy(&efs, (void *) val, olen);
break;
...
}
done:
...
if (result == L2CAP_CONF_SUCCESS) {
...
if (remote_efs) {
if (chan->local_stype != L2CAP_SERV_NOTRAFIC &&
efs.stype != L2CAP_SERV_NOTRAFIC && //【2】
efs.stype != chan->local_stype) {
...
} else {
/* Send PENDING Conf Rsp */
result = L2CAP_CONF_PENDING;
set_bit(CONF_LOC_CONF_PEND, &chan->conf_state); //這里設置CONF_LOC_CONF_PEND
}
}
}
當收到L2CAPCONFREQ的包中包含有L2CAPCONFEFS類型的數據【1】,而且L2CAPCONFEFS數據的stype == L2CAPSERVNOTRAFIC【2】的時候,confstate會被置CONFLOCCONFPEND
到這里,這個漏洞觸發的思路也就清楚了:
1,建立和目標機器的L2CAP 連接,這里注意socktype的選擇要是SOCKRAW,如果不是,內核會自動幫我們完成sentinfomationrequest , sendconnectionrequest, sendconfigurerequest這些操作,也就無法觸發目標機器的漏洞了。
2,建立SOCKRAW連接,connect的時候,會自動完成sentinfomationrequest的操作,不過這個不影響。
3,接下來我們需要完成sendconnectionrequest操作,來確定SCID,DCID。完成這個操作的過程是發送合法的 L2CAPCONNREQ數據包。
4,接下來需要發送包含有L2CAPCONFEFS類型的數據,而且L2CAPCONFEFS數據的stype == L2CAPSERVNOTRAFIC的L2CAPCONFREQ包,這一步是為了讓目標機器的confstate變成CONFLOCCONFPEND。
5,這里就到了發送cmdlen很長的L2CAPCONNRSP包了。這個包的result字段需要是L2CAPCONFPENDING。那么這個包發過去之后,目標機器就內核棧溢出了,要么重啟了,要么死機了。
這個漏洞是這幾個漏洞里,觸發最難的。
CVE-2017-1000250
這個漏洞是BlueZ的SDP服務里的信息泄露漏洞。這個不像L2CAP層的連接那么復雜,主要就是上層服務,收到數據就進行處理。那么我們也只需要關注處理的函數。 之前說過,BlueZ的SDP收到數據是從iosessionevent開始。之后,數據的流向是:
iosessionevent-->handlerequest-->processrequest

有必要介紹一下SDP協議的數據結構: 它有一個sdppudhdr的頭部,頭部數據里定義了PUD命令的類型,tid,以及pdu parameter的長度,然后就是具體的parameter。最后一個字段是continuation state,當一個包發不完所要發送的數據的時候,這個字段就會有效。對與這個字段,BlueZ給了它一個定義:
typedef struct {
uint32_t timestamp;
union {
uint16_t maxBytesSent;
uint16_t lastIndexSent;
} cStateValue;
} sdp_cont_state_t;
對于遠程的連接,PDU命令類型只能是這三個:SDPSVCSEARCHREQ, SDPSVCATTRREQ, SDPSVCSEARCHATTRREQ。這個漏洞呢,出現在對SDP_SVCSEARCHATTRREQ命令的處理函數里面 servicesearchattrreq 。這個函數有點長,就直接說它干了啥,不貼代碼了:
1, extractdes(pdata, dataleft, &pattern, &dtd, SDPTYPEUUID); 解析service search pattern(對應SDP協議數據結構圖)
2,max = getbe16(pdata); 獲得Maximu Attribute Byte
3,scanned = extractdes(pdata, dataleft, &seq, &dtd, SDPTYPEATTRID);解析Attribute ID list
4,if (sdpcstateget(pdata, dataleft, &cstate) < 0) ;獲取continuation state狀態cstate,如果不為0,則將包里的continuation state數據復制給cstate.
漏洞發生在對cstate狀態不為0的時候的處理,我們重點看這部分的代碼:
sdp_buf_t *pCache = sdp_get_cached_rsp(cstate);
if (pCache) {
uint16_t sent = MIN(max, pCache->data_size - cstate->cStateValue.maxBytesSent);
pResponse = pCache->data;
memcpy(buf->data, pResponse + cstate->cStateValue.maxBytesSent, sent); //【1】
buf->data_size += sent;
cstate->cStateValue.maxBytesSent += sent;
if (cstate->cStateValue.maxBytesSent == pCache->data_size)
cstate_size = sdp_set_cstate_pdu(buf, NULL);
else
cstate_size = sdp_set_cstate_pdu(buf, cstate);
sdpgetcachedrsp函數其實是對cstate的timestamp值的檢驗,如何過這個檢驗之后再說。當代碼走到【1】處的memcpy時,由于cstate->maxBytesSent就是由數據包里的數據所控制,而且沒有做任何檢驗,所以這里可以為任意的uint16t值。那么很明顯,這里就出現了一個對pResponse的越界讀的操作。而越界讀的數據還會通過SDP RESPONSE發送給攻擊方,那么一個信息泄露就發生了。
寫這個poc需要注意sdpgetcachedrsp的檢驗的繞過,那么首先需要得到一個timestamp。當一次發送的包不足以發送完所有的數據的時候,會設置cstate狀態,所以如果我們發給服務端的包里,max字段非常小,那么服務端就會給我們回應一個帶cstate狀態的包,這里面會有timestamp:
if (cstate == NULL) {
...
if (buf->data_size > max) { //max 可由接收到的包數據指定
sdp_cont_state_t newState;
memset((char *)&newState, 0, sizeof(sdp_cont_state_t));
newState.timestamp = sdp_cstate_alloc_buf(buf); //這里得到一個timestamp
buf->data_size = max;
newState.cStateValue.maxBytesSent = max;
cstate_size = sdp_set_cstate_pdu(buf, &newState); //回應的包中,寫上cstate狀態。
} else
cstate_size = sdp_set_cstate_pdu(buf, NULL);
所以,我們的poc應該是這個步驟:
1,建立SDP連接。這里我們的socket需要是SOCK_STREAM類型,而且connet的時候,addr的psm字段要是0x0001。關于連接的PSM:

2,發送一個不帶cstate狀態的數據包,而且指定Maximu Attribute Byte的值非常小。這一步是為了讓服務端給我們返回一個帶timestamp的包。
3,接收這個帶timestamp的包,并將timestamp提取。
4,發送一個帶cstate狀態的數據包,cstate的timestamp是指定為提取出來的值,服務端memcpy的時候,則就會把pResponse+maxBytesSent的內容發送給我們,讀取這個數據包,則就獲取了泄露的數據。
CVE-2017-0785
這個漏洞也是SDP的信息泄露漏洞,不過是BlueDroid的。與BlueZ的那個是有些類似的。我們也從對SDP數據包的處理函數說起。 SDP數據包會通過sdpdataind函數送給sdpserverhandleclientreq。與BlueZ一樣,這個函數也會根據包中的pudid來確定具體的處理函數。這個漏洞發生在對SDPPDUSERVICESEARCH_REQ命令的處理,對包內數據的解析與上文BlueZ中的大同小異,不過注意在BlueDroid中,cstate結構與BlueZ中有些不同:
typedef struct {
uint16_t cont_offset;
} sdp_cont_state_t;
這里主要看漏洞:
①, BE_STREAM_TO_UINT16 (max_replies, p_req);從包中解析出Maximu Attribute Byte
②, for (num_rsp_handles = 0; num_rsp_handles < max_replies; )
{
p_rec = sdp_db_service_search (p_rec, &uid_seq);
if (p_rec)
rsp_handles[num_rsp_handles++] = p_rec->record_handle;
else
break;
}
③, /* Check if this is a continuation request */
if (*p_req)
{
if (*p_req++ != SDP_CONTINUATION_LEN || (p_req >= p_req_end))
{
sdpu_build_n_send_error (p_ccb, trans_num, SDP_INVALID_CONT_STATE,
SDP_TEXT_BAD_CONT_LEN);
return;
}
BE_STREAM_TO_UINT16 (cont_offset, p_req); //從包中得到cont_offset
if (cont_offset != p_ccb->cont_offset) //對cont_offset的檢驗
{
sdpu_build_n_send_error (p_ccb, trans_num, SDP_INVALID_CONT_STATE,
SDP_TEXT_BAD_CONT_INX);
return;
}
rem_handles = num_rsp_handles - cont_offset; /* extract the remaining handles */
}
else
{
rem_handles = num_rsp_handles;
cont_offset = 0;
p_ccb->cont_offset = 0;
}
④, cur_handles = (UINT16)((p_ccb->rem_mtu_size - SDP_MAX_SERVICE_RSPHDR_LEN) / 4);
if (rem_handles <= cur_handles)
cur_handles = rem_handles;
else /* Continuation is set */
{
p_ccb->cont_offset += cur_handles;
is_cont = TRUE;
}
⑤, for (xx = cont_offset; xx < cont_offset + cur_handles; xx++)
UINT32_TO_BE_STREAM (p_rsp, rsp_handles[xx]);
①,②中代碼可以看出,變量numrsphandles的值,一定程度上可以由包中的Maximu Attribute Byte字段控制。 ③中代碼是對帶cstate的包的處理,第一步是對大小的檢查,第二步是獲得contoffset,然后對contoffset進行檢查,第三步就到了 remhandles = numrsphandles - contoffset 可以思考一種情況,如果numrsphandles < contoffset,那么這個代碼就會發生整數的下溢,而numrsphandles在一定程度上我們可以控制,而且是可以控制它變成0,那么只要contoffset不為0,這里就會發生整數下溢。發生下溢的結果給了remhandles,而這個變量代表的是還需要發送的數據數。 在④中,如果remhandles是發生了下溢的結果,由于它是uint16t類型,那么它將變成一個很大的數,所以會走到 pccb->contoffset += curhandles;,curhandles是一個固定的值,那么如果這個下溢的過程,發生很多次,pccb->contoffset就會變得很大,那么在5處,就會有一個對rsphandles數組的越界讀的產生。
下面的操作可以讓這個越界讀發生:
1,發送一個不帶cstate的包, 而且Maximu Attribute Byte字段設置的比較大。那么結果就是remhandles = numrsphandles,而由于maxreplies比較大,所以numrsphandles會成為一個比較大的值。只要在④中保證remhandles > curhandles,那么pccb->contoffset就會成為一個非0值curhandles。這一步是為了使得pccb->contoffset成為一個非0值。
2,接收服務端的回應包,這個回應包里的cstate字段將會含有剛剛的pccb->contoffset值,我們取得這個值。
3,發送一個帶cstate的包,contoffset指定為剛剛提取的值,而且設置Maximu Attribute Byte字段為0。那么服務端收到這個包后,就會走到remhandles = numrsphandles - contoffset 從而發生整數下溢,同時pccb->contoffset又遞增一個cur_handles大小。
4,重復2和3的過程,那么pccb->contoffset將越來越大,從而在⑤出發生越界讀,我們提取服務端返回的數據,就可以獲得泄露的信息的內容。
CVE-2017-0781
現在我們到了BNEP服務。BNEP的協議格式,下面兩張圖可以說明的很清楚:

BlueDroid中BNEP服務對于接受到的數據包的處理也不復雜:
1,解析得到BNEPTYPE,得到extension位。
2,檢查連接狀態,如果已經連接則后續可以處理非BNEPFRAMECONTROL的包,如果沒有建立連接,則后續只處理BNEPFRAMECONTROL的包。
3,去BNEPTYPE對應的處理函數進行處理。
4,對于BNEPTYPE不是BNEPFRAME_CONTROL而且有extension位的,還需要對extension的數據進行處理。
5,調用pan層的回調函數。
值得注意的是,BNEP連接真正建立起來,需要先處理一個合法的BNEPFRAMECONTROL數據包。 CVE-2017-0781正是連接還沒建立起來,在處理BNEPFRAMECONTROL時所發生的問題:
case BNEP_FRAME_CONTROL:
ctrl_type = *p;
p = bnep_process_control_packet (p_bcb, p, &rem_len, FALSE);
if (ctrl_type == BNEP_SETUP_CONNECTION_REQUEST_MSG &&
p_bcb->con_state != BNEP_STATE_CONNECTED &&
extension_present && p && rem_len)
{
p_bcb->p_pending_data = (BT_HDR *)osi_malloc(rem_len);
memcpy((UINT8 *)(p_bcb->p_pending_data + 1), p, rem_len);
p_bcb->p_pending_data->len = rem_len;
p_bcb->p_pending_data->offset = 0;
}
上述代碼中,malloc了一個remlen的大小,這個是和收到的數據包的長度相關的。可是memcpy的時候,卻是從pbcb->ppendingdata+1開始拷貝數據,那么這里會直接溢出一個sizeof(*(pbcb->ppendingdata))大小的內容。這個大小是8.所以只要代碼走到這,就會有一個8字節大小的堆溢出。而要走到這,只需要過那個if的判斷條件,而這個if其實是對BNEPSETUPCONNECTIONREQUESTMSG命令處理失敗后的錯誤處理函數。那么只要發送一個錯誤的BNEPSETUP_CONNECTIONREQUESTMSG命令包,就可以進入到這段代碼了觸發堆溢出了。
所以我們得到poc的編寫過程:
1,建立BNEP連接,這個和SDP類似,只是需要指定PSM為BNEP對應的0x000F。
2,發送一個BNEPTYPE為BNEPFRAMECONTROL,extension字段為1,ctrltype為BNEPSETUPCONNECTIONREQUESTMSG的錯誤的BNEP包:

CVE-2017-0782
這個也是由于BNEP協議引起的漏洞,首先它是個整數溢出,整數溢出導致的后果是堆溢出。 問題出在BNEP對extension字段的處理上:
UINT8 *bnep_process_control_packet (tBNEP_CONN *p_bcb, UINT8 *p, UINT16 *rem_len, BOOLEAN is_ext)
{
UINT8 control_type;
BOOLEAN bad_pkt = FALSE;
UINT16 len, ext_len = 0;
if (is_ext)
{
ext_len = *p++; 【1】
*rem_len = *rem_len - 1;
}
control_type = *p++;
*rem_len = *rem_len - 1;
switch (control_type)
{
...
default :
bnep_send_command_not_understood (p_bcb, control_type);
if (is_ext)
{
p += (ext_len - 1);
*rem_len -= (ext_len - 1); 【2】
}
break;
}
if (bad_pkt)
{
BNEP_TRACE_ERROR ("BNEP - bad ctl pkt length: %d", *rem_len);
*rem_len = 0;
return NULL;
}
return p;
}
上述代碼中,【1】的ext_len從數據包中獲得,沒有長度的檢查,可為任意值。而當control_type為一個非法值的時候,會走到【2】,那么這里就很有說法了,我們如果設置ext_len比較大,那么這里就會發生一個整數下溢。從而使得rem_len變成一個很大的uint16_t的值。這個值將會影響后續的處理:
while (extension_present && p && rem_len)
{
ext_type = *p;
extension_present = ext_type >> 7;
ext_type &= 0x7F;
...
p++;
rem_len--;
p = bnep_process_control_packet (p_bcb, p, &rem_len, TRUE); 【1】
}
p_buf->offset += p_buf->len - rem_len;
p_buf->len = rem_len; 【2】
...
if (bnep_cb.p_data_buf_cb)
{
(*bnep_cb.p_data_buf_cb)(p_bcb->handle, p_src_addr, p_dst_addr, protocol, p_buf, fw_ext_present); 【3】
}
...
osi_free(p_buf);
}
上面的代碼中,【1】處將發生整數下溢出,使得rem_len成為一個很大的值(比如0xfffd),【2】處會將這個值賦值給p_buf->len。【3】處是回調函數處理這個p_buf,在BlueDroid中這個函數是pan_data_buf_ind_cb,這個函數會有一條路徑調到bta_pan_data_buf_ind_cback,而在這個函數中:
static void bta_pan_data_buf_ind_cback(UINT16 handle, BD_ADDR src, BD_ADDR dst, UINT16 protocol, BT_HDR *p_buf,
BOOLEAN ext, BOOLEAN forward)
{
tBTA_PAN_SCB *p_scb;
BT_HDR *p_new_buf;
if (sizeof(tBTA_PAN_DATA_PARAMS) > p_buf->offset) {
/* offset smaller than data structure in front of actual data */
p_new_buf = (BT_HDR *)osi_malloc(PAN_BUF_SIZE);
memcpy((UINT8 *)(p_new_buf + 1) + sizeof(tBTA_PAN_DATA_PARAMS),
(UINT8 *)(p_buf + 1) + p_buf->offset, p_buf->len);
p_new_buf->len = p_buf->len;
p_new_buf->offset = sizeof(tBTA_PAN_DATA_PARAMS);
osi_free(p_buf);
} else {
...
}
memcpy用到了我們傳進來的pbuf,而pbuf->len是剛剛下溢之后的很大的值,所以主要保證sizeof(tBTAPANDATAPARAMS) > pbuf->offset,這里就會發生一次很大字節的堆溢出。
代碼首先要走到extension的處理,這個的前提是連接狀態是BNEPSTATECONNECTED。而這個狀態的建立,需要服務端先接收一個正確的BNEPSETUPCONNECTIONREQUESTMSG請求包,同時要想pandatabufindcb調用到bta_pandatabufindcback產生堆溢出,需要在建立連接的時候指定UUID為UUIDSERVCLASSPANU可以閱讀這兩個函數來找到這樣做的原因,這里就不再貼代碼了。清楚這一點之后,我們就可以構造我們的poc了:
1,建立BNEP連接,這里只是建立起初步的連接,connstate還不是BNEPSTATECONNECTED,這一步通過connect實現
2,發送一個正確的BNEPSETUPCONNECTIONREQUESTMSG請求包,同時指定UUID為UUIDSERVCLASSPANU。這個包將是這樣子:

3,發送一個extension字段可導致整數下溢的包,而且注意控制pbuf->offset變得比較小:

這樣PoC就完成了。 CVE-2017-0781和CVE-2017-0782導致了堆溢出,一般會使得com.android.bluetooth崩潰,但是這個進程崩潰系統不會有提醒,需要去logcat來找崩潰的日志。這是兩個很有品質的堆溢出漏洞,結合前面的信息泄露漏洞,是完全可以轉化為遠程代碼執行的。
0x03
這篇分析到這里也就結束了,藍牙出漏洞是個比較危險的事情,希望沒有修補的能盡快修補,補丁鏈接如下:
確定自己是否有漏洞可以用我們提供的poc呀,關于藍牙漏洞的研究,也希望能和各位多多交流。
參考文檔:
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/408/
暫無評論