作者: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.cio_session_event函數,來獲取這個數據包,交由sdp-request.chandle_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.csdpserverhandleclientreq函數進行處理:

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_channell2cap_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呀,關于藍牙漏洞的研究,也希望能和各位多多交流。

參考文檔:

  1. https://www.armis.com/blueborne/

  2. http://blog.csdn.net/rain0993/article/details/8533246

  3. https://people.csail.mit.edu/albert/bluez-intro/index.html


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/408/