CVE-2020-0022 an Android 8.0-9.0 Bluetooth Zero-Click RCE – BlueFrag

Nowadays, Bluetooth is an integral part of mobile devices. Smartphones interconnect with smartwatches and wireless headphones. By default, most devices are configured to accept Bluetooth connections from any
nearby unauthenticated device. Bluetooth packets are processed by the Bluetooth chip (also called a controller), and then passed to the host (Android, Linux, etc.). Both, the firmware on the chip and the host Bluetooth subsystem, are a target for Remote Code Execution (RCE) attacks.

One feature that is available on most classic Bluetooth implementations is answering over Bluetooth pings. Everything an attacker needs to know is the device’s Bluetooth address. Even if the target is not discoverable, it typically accepts connections if it gets addressed. For example, an attacker can run l2ping, which establishes an L2CAP connection and sends echo requests to the remote target.

In the following, we describe a Bluetooth zero-click short-distance RCE exploit against Android 9, which got assigned CVE-2020-0022 . We go through all steps required to establish a remote shell on a Samsung Galaxy S10e, which was working on an up-to-date Android 9 when reporting the issue on November 3 2019. The initial flaw used for this exploit is still present in Android 10, but we utilize an additional bug in Bionic (Android’s libc implementation), which makes exploitation way easier. The bug was finally fixed in the security patch from 1.2.2020 in A-143894715. Here is a demo of the full proof of concept:

During the work on InternalBlue and Frankenstein at SEEMOO, we spent a lot of time investigating the Braodcom Bluetooth firmware. InternalBlue was initially written by Dennis Mantz, and it interacts with the firmware to add debugging capabilities. Within this project, a lot of reverse engineering to understand the details of the firmware itself was done.

For further analysis, we built Frankenstein, which emulates the firmware for fuzzing. To achieve emulation, an essential part is understanding the Bluetooth Core Scheduler (BCS). This component is of interest, as it also processes the packet and payload header, and manages time-critical tasks. These low-level functions are not accessible from the host, and not even within the threaded components of the firmware itself. By accessing the BCS, we were even able to inject raw wireless frames into the emulated firmware.

When fuzzing with Frankenstein, we focused on vulnerabilities that arise prior to pairing. In these parts of the protocol, we found two vulnerabilities, one in classic Bluetooth and one in Bluetooth Low Energy (BLE). The first heap overflow is in the processing of Bluetooth scan results (EIR packets), affecting firmware with build dates in the range 2010-2018, possibly even older (CVE-2019-11516). For this, we provided a full RCE Proof-of-Concept (PoC) to Broadcom in April 2019. After the report, Broadcom claimed that they knew of the issue, and indeed, the newest Samsung Galaxy S10e had a patch that we were not aware of, as it just had been released. The second heap overflow affects all BLE Packet Data Units (PDUs) since Bluetooth 4.2. We provided a PoC to Broadcom in June 2019, which corrupts the heap but misses one primitive
that would be achieved with more data throughput. To the best of our knowledge, this issue has not been fixed as of February 2020.

While working on PoCs and ideas on how to get a lot of data into the heap, we also looked into classic Bluetooth Asynchronous Connection-Less (ACL) packets. These are primarily used for data transfer, such as music streaming, tethering, or, more general, L2CAP. Within the firmware, ACL processing is comparably simple. There are way more sophisticated handlers and proprietary protocol extensions, for example, Jiska Classen found a Link Mangement Protocol (LMP) type confusion (CVE-2018-19860).

The bug described in this post was triggered within ACL. We fuzzed this protocol by performing bit flips on the packet and payload header. The initial fuzzer was implemented by hooking the function bcs_dmaRxEnable within the firmware, which is invoked by the BCS ACL task. bcs_dmaRxEnable copies wireless frames into the transmit buffer. Prior to this function, the packet and payload headers are already written to the corresponding hardware registers. We are therefore able to modify the full packet before transmission and thus building a simple
Bluetooth fuzzer within the firmware.

In the initial setup, we run l2ping on a Linux host against an Android device over-the-air, and the Bluetooth firmware fuzzer flips bits randomly in the headers. While we were trying to crash the firmware of the Android device, instead, the Android Bluetooth daemon crashed. In the logs, we observe several crash reports like this:

pid: 14808, tid: 14858, name: HwBinder:14808_  >>> <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x79cde00000
    x0  00000079d18360e1  x1  00000079cddfffcb  x2  fffffffffff385ef  x3  00000079d18fda60
    x4  00000079cdd3860a  x5  00000079d18360df  x6  0000000000000000  x7  0000000000000000
    x8  0000000000000000  x9  0000000000000000  x10 0000000000000000  x11 0000000000000000
    x12 0000000000000000  x13 0000000000000000  x14 ffffffffffffffff  x15 2610312e00000000
    x16 00000079bf1a02b8  x17 0000007a5891dcb0  x18 00000079bd818fda  x19 00000079cdd38600
    x20 00000079d1836000  x21 0000000000000097  x22 00000000000000db  x23 00000079bd81a588
    x24 00000079bd819c60  x25 00000079bd81a588  x26 0000000000000028  x27 0000000000000041
    x28 0000000000002019  x29 00000079bd819df0
    sp  00000079bd819c50  lr  00000079beef4124  pc  0000007a5891ddd4

    #00 pc 000000000001ddd4  /system/lib64/ (memcpy+292)
    #01 pc 0000000000233120  /system/lib64/ (reassemble_and_dispatch(BT_HDR*) [clone .cfi]+1408)
    #02 pc 000000000022fc7c  /system/lib64/ (BluetoothHciCallbacks::aclDataReceived(android::hardware::hidl_vec<unsigned char> const&)+144)

It seems, that memcpy is executed with a negative length inside reassemble_and_dispatch. A simplified implementation of memcpy looks as follows:

void *memcpy(char *dest; char *src, size_t *n) {
  for (size_t i=0; i<n; i++)
    dst[i] = src[i];

The length parameter n is of type size_t and, thus, an unsigned integer. If we pass a negative number as n, it will be interpreted as a large positive number because of the twos-complement representation.
As a result, memcpy tries to copy memory in an endless loop, which causes the crash as soon as we hit unmapped memory.

Bluetooth implements fragmentation on various layers. Within the analysis of this crash, we focus on the fragmentation of L2CAP packets passed between the controller and the host. For commands and configuration between host and controller, the Host Controller Interface (HCI) is used.

L2CAP is sent as ACL packets via the same UART wires as HCI. It needs to be fragmented to the maximum ACL packet length. During firmware initialization by the driver on the host, the HCI command Read Buffer Size. On Broadcom chips, this size is 1021. The host’s driver needs to respect these size limits when sending packets to the firmware. Similarly, the firmware also rejects L2CAP inputs that are not properly fragmented over the air. As fragmentation and reassembling happens on the host, but the firmware itself also has strict size limits, L2CAP is interesting for heap exploitation on the host and the controller.

If a L2CAP packet is received, which length is longer than the maximum buffer size of 1021, it has to be reassembled. The partial packet is stored in a map called partial_packets with the connection handle as the key. A buffer that is sufficiently large to hold the final packet is allocated, and the received data is copied to that buffer. The end of the last received fragment is stored in partial_packet->offset.

The following packets have the continuation flag set to indicate that this is a packet fragment. It is the 12th bit in the connection handle inside the ACL header. If such a packet is received, the packet content is then copied to the previous offset.

static void reassemble_and_dispatch(UNUSED_ATTR BT_HDR *packet) {
      packet->offset = HCI_ACL_PREAMBLE_SIZE;
      uint16_t projected_offset =
          partial_packet->offset + (packet->len - HCI_ACL_PREAMBLE_SIZE);
      if (projected_offset >
          partial_packet->len) {  // len stores the expected length
             "%s got packet which would exceed expected length of %d."
             __func__, partial_packet->len);
        packet->len = partial_packet->len - partial_packet->offset;
        projected_offset = partial_packet->len;
      memcpy(partial_packet->data + partial_packet->offset,
         packet->data + packet->offset, packet->len - packet->offset);


This step causes the negative length memcpy, as seen in the code above. In a situation where we get a packet and there are only 2 bytes left to receive, if the continuation is longer than expected, packet->length is truncated to avoid buffer overflows. The length is set to the number of bytes that are left to copy.

As we need to skip the HCI and ACL preamble, we use HCI_ACL_PREAMBLE_SIZE (4) as the packet offset and subtract it from the number of bytes to copy. This results in a negative length of -2 for memcpy.

This has been addressed in master but at this point in time not inandroid10-c2f2-release branch, android10-dev branch, android-10.0.0_r9 tag or the android-9.0.0_r49 tag running on the S10e at that point in time. A fix has been deployed in android-8.0.0_r43, android-8.1.0_r73, android-9.0.0_r53 and android-10.0.0_r29.

The above-mentioned bug seems not to be exploitable, as we end up in an endless memcpy.  Yet, we occasionally crash in completely different locations. For example, the following crash is located in the same thread, but cannot be explained with a simple infinite copy loop. Thus, we expected to find a different bug somewhere.

pid: 14530, tid: 14579, name: btu message loo  >>> <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7a9e0072656761
    x0  0000007ab07d72c0  x1  0000007ab0795600  x2  0000007ab0795600  x3  0000000000000012
    x4  0000000000000000  x5  0000007a9e816178  x6  fefeff7a3ac305ff  x7  7f7f7f7f7fff7f7f
    x8  007a9e0072656761  x9  0000000000000000  x10 0000000000000020  x11 0000000000002000
    x12 0000007aa00fc350  x13 0000000000002000  x14 000000000000000d  x15 0000000000000000
    x16 0000007b396f6490  x17 0000007b3bc46120  x18 0000007a9e81542a  x19 0000007ab07d72c0
    x20 0000007ab0795600  x21 0000007a9e817588  x22 0000007a9e817588  x23 000000000000350f
    x24 0000000000000000  x25 0000007ab07d7058  x26 000000000000008b  x27 0000000000000000
    x28 0000007a9e817588  x29 0000007a9e816340
    sp  0000007a9e8161e0  lr  0000007a9fde0ca0  pc  0000007a9fe1a9a4

    #00 pc 00000000003229a4  /system/lib64/ (list_append(list_t*, void*) [clone .cfi]+52)
    #01 pc 00000000002e8c9c  /system/lib64/ (l2c_link_check_send_pkts(t_l2c_linkcb*, t_l2c_ccb*, BT_HDR*) [clone .cfi]+100)
    #02 pc 00000000002ea25c  /system/lib64/ (l2c_rcv_acl_data(BT_HDR*) [clone .cfi]+1236)

We spent a couple of sleepless nights tracking down these crashes and modified the fuzzing setup to be reproducible. However, it was not possible to reproduce these interesting crashes by replaying packets. The main issue during debugging was that we did not compile Android with an address sanitizer. This would have detected any memory corruption as it happens before crashing in a random location. So, in a moment of frustration, we decided to cheat a little bit. By keeping the payload of the L2Ping packets constant, we can compare it with the payload of the response. If the data changes meanwhile, a memory corruption took place but did not produce a crash yet. After running this for a while, we get corrupted responses like this:

With this detection method, we were even able to reproduce this behavior reliably. The following packet combination triggers it:

  1. L2cap packet with 2 bytes remaining for continuation ‘A’s
  2. Continuation longer than the expected 2 bytes containing ‘B’s

In Android logcat, we can observe the following error message:

bt_hci_packet_fragmenter: reassemble_and_dispatch got packet which would
 exceed expected length of 147. Truncating.

This trigger looks similar to the bug described above. Note that only the last bytes are corrupted, whereas the beginning of the packet is still correct. This behavior cannot be explained by the source code and what we know so far. A straight buffer overflow that keeps the first couple of bytes intact or overwrites pointers and offsets in such a controlled manner is rather unlikely. At this point, we decided to set breakpoints in the packet_fragmenter to observe where the packet data is modified. We used the following GDB script to debug that behavior, whereas reassemble_and_dispatch+1408 and reassemble_and_dispatch+1104 are the two memcpy in reassemble_and_dispatch as described earlier.

b reassemble_and_dispatch
commands; x/32x $x0; c; end

b dispatch_reassembled
commands; x/i $lr; x/32x $x0; c; end

b *(reassemble_and_dispatch+1408)
commands; p $x0; p $x1;p $x2; c; end

b *(reassemble_and_dispatch+1104)
commands; p $x0; p $x1; p $x2; c; end

For the first packet containing ‘A’s, we can observe the following log. It is received as expected, and the first memcpy is triggered with a length of 0x52 bytes. This length is also visible in the BT_HDR struct inside the packet and is correct. The length included in the ACL and L2CAP header is two bytes longer than the actual payload to trigger the packet reassembling of the packet. The connection handle in the HCI header is 0x200b and indicates a start packet for the connection handle 0x0b.

The second packet also arrives correctly in reassemble_and_dispatch and the connection handle has changed to 0x100b and indicates a continuation packet. The third argument to memcpy is 0xfffffffffffffffe aka -2 as pointed out above. As memcpy treats the third parameter as an unsigned integer, this memcpy will result in a crash.

But apparently, the application continues and corrupts the last 66 bytes of the partial packet and the corrupted packet is passed to dispatch_reassembled.


If we have a closer look at the actual memcpy implementation it is more complex than the simple character-wise memcpy shown above. It is more efficient to copy whole words of memory instead of individual bytes. This implementation takes it one step further and fills registers with 64 bytes of memory content before writing it to the target location. Such an implementation is more complex and has to consider edge cases such as odd lengths and misaligned addresses.

There exists a weird behavior in that memcpy implementation regarding negative lengths. As we try to copy to the end of the destination buffer, we overwrite the last 66 bytes of the L2Ping request with whatever is previous of our second packet. We have written this short PoC to test the memcpy behavior.

int main(int argc, char **argv) {
    if (argc < 3) {
        printf("usage %s offset_dst offset_srcn", argv[0]);

    char *src = malloc(256);
    char *dst = malloc(256);

    printf("src=%pn", src);
    printf("dst=%pn", dst);

    for (int i=0; i<256; i++) src[i] = i;
    memset(dst, 0x23, 256);

    memcpy( dst + 128 + atoi(argv[1]),
            src + 128 + atoi(argv[2]),
            0xfffffffffffffffe );

    for(int i=0; i<256; i+=32) {
        printf("%04x:  ", i);
        for (int j=0; j<32; j++) {
            printf("%02x", dst[i+j] & 0xff);
            if (j%4 == 3) printf(" ");

The behavior was analyzed in Unicorn emulating the aarch64 memcpy implementation. The relevant code is shown in the following:

prfm    PLDL1KEEP, [src]
add srcend, src, count
add dstend, dstin, count
cmp     count, 16    L(copy16)           //Not taken as 0xfffffffffffffffe > 16
cmp count, 96
b.hi    L(copy_long)        //Taken as as 0xfffffffffffffffe > 96


and tmp1, dstin, 15         //tmp1 = lower 4 bits of destination
bic dst, dstin, 15
ldp D_l, D_h, [src]
sub src, src, tmp1
add count, count, tmp1      /* Count is now 16 too large.  */
                            //It is not only too large
                            //but might also be positive!
                            //0xfffffffffffffffe + 0xe = 0xc
ldp A_l, A_h, [src, 16]
stp D_l, D_h, [dstin]
ldp B_l, B_h, [src, 32]
ldp C_l, C_h, [src, 48]
ldp D_l, D_h, [src, 64]!
subs    count, count, 128 + 16  /* Test and readjust count.  */
                                //This  will become negative again    2f                      //So this branch is taken


/* Write the last full set of 64 bytes.  The remainder is at most 64
bytes, so it is safe to always copy 64 bytes from the end even if
there is just 1 byte left.  */
//This will finally corrupt -64...64 bytes and terminate
ldp E_l, E_h, [srcend, -64]
stp A_l, A_h, [dst, 16]
ldp A_l, A_h, [srcend, -48]
stp B_l, B_h, [dst, 32]
ldp B_l, B_h, [srcend, -32]
stp C_l, C_h, [dst, 48]
ldp C_l, C_h, [srcend, -16]
stp D_l, D_h, [dst, 64]
stp E_l, E_h, [dstend, -64]
stp A_l, A_h, [dstend, -48]
stp B_l, B_h, [dstend, -32]
stp C_l, C_h, [dstend, -16]

As we are dealing with a very large count value (INT_MAX – 2), it will always be larger than the distance between dst and src. Therefore __memcpy will never be called in our case, which makes this bug  unexploitable on Android 10.

As described above, we can essentially overwrite the last 64 bytes of the packet with whatever happens to be in front of our source address. The first 20 bytes before the source buffer are always BT_HDR, acl_hdr, and l2cap_hdr. We therefore automatically leak the connection handle of the remote device.

The content of the uninitialized memory depends on the placement of the second packet buffer and therefore its size. By repeatedly sending regular L2Ping echo requests, we can try to place our own
packet data in front of the second packet. This allows us to control the last 44 bytes of the packet with arbitrary data. By shortening the first packet, we can control the full packet struct including the headers. The first packet looks like the following:


After triggering the bug, the corrupted packet looks like the following. The packet containing the ‘X’ is the one we have placed in front of our source buffer. Note, that except for the length in BT_HDR, the packet length is now 0x280 instead of 0x30. The packet->len field must be still the original length, otherwise, the reassemble method would expect further data.


This results in a much more useful leak. Note that this is a data-only attack without code execution or any other additional information required. It can also be used to inject arbitrary L2CAP traffic into any active connection handles. A successful leak might look as follows:

In order to defeat Address Space Layout Randomization (ASLR), we need the base address of some libraries. Occasionally we find an object from on the heap, which has the following structure:

  • Some heap pointer
  • Pointer to uhash_hashUnicodeString_60 at
  • Pointer to uhash_compareUnicodeString_60 at
  • Pointer to uhash_compareLong_60 at
  • Pointer to uprv_deleteUObject_60 at

We can use the offsets between those functions to reliable detect this structure within the leak. This allows us to compute the base address for .

Several libraries, such as, are protected by Clang’s Call Flow Integrity (CFI) implementation. This protects forward edges and should prevent us from simply overwriting function vtables on the heap with arbitrary addresses. Only functions that belong to the affected object should be callable. Even though, upon disconnect, we occasionally trigger the following crash when after corrupting the heap.

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x37363534333231
    x0  3837363534333231  x1  000000750c2649e0  x2  000000751e50a338  x3  0000000000000000
    x4  0000000000000001  x5  0000000000000001  x6  00000075ab788000  x7  0000000001d8312e
    x8  00000075084106c0  x9  0000000000000001  x10 0000000000000001  x11 0000000000000000
    x12 0000000000000047  x13 0000000000002000  x14 000f5436af89ca08  x15 000024747b62062a
    x16 000000750c2f55d8  x17 000000750c21b088  x18 000000750a660066  x19 000000751e50a338
    x20 000000751e40dfb0  x21 000000751e489694  x22 0000000000000001  x23 0000000000000000
    x24 000000750be85f64  x25 000000750a661588  x26 0000000000000005  x27 00000075084106b4
    x28 000000750a661588  x29 000000750a65fd30
    sp  000000750a65fd10  lr  000000750c264bb8  pc  000000750c264c5c
    #00 pc 00000000000dbc5c  /system/lib64/ (base::WaitableEvent::Signal()+200)
    #01 pc 00000000000add88  /system/lib64/ (base::internal::IncomingTaskQueue::PostPendingTask(base::PendingTask*)+320)
    #09 pc 00000000002dd0a8  /system/lib64/ (L2CA_DisconnectRsp(unsigned short) [clone .cfi]+84)
    #10 pc 0000000000307a08  /system/lib64/ (sdp_disconnect_ind(unsigned short, bool) [clone .cfi]+44)
    #11 pc 00000000002e39d4  /system/lib64/ (l2c_csm_execute(t_l2c_ccb*, unsigned short, void*) [clone .cfi]+5500)
    #12 pc 00000000002eae04  /system/lib64/ (l2c_rcv_acl_data(BT_HDR*) [clone .cfi]+4220)

During the leak process, we do not only overflow into the negative direction, but also corrupt data that is stored after the affected buffer. In this particular case, we have overwritten a pointer stored in X0.
By looking at the location in the code, we crash the instructions before a branch register controlled by X0.

dbc5c: f9400008 ldr x8, [x0] // We control X0
dbc60: f9400108 ldr x8, [x8]
dbc64: aa1403e1 mov x1, x20
dbc68: d63f0100 blr x8 // Brach to **X0

If we know an address where we can store arbitrary data, we can control pc! was not compiled with CFI enabled. Our packet data has to be stored on the heap somewhere, but we also need a way to retrieve the address, to gain RCE. This was achieved as the partial packets are stored in a hash map with the connection handle as a key:

BT_HDR* partial_packet =
         (BT_HDR*)buffer_allocator->alloc(full_length + sizeof(BT_HDR));
memcpy(partial_packet->data, packet->data, packet->len);
partial_packets[handle] = partial_packet;

This will allocate a map object on the heap, holding the key (handle) and a pointer to our packet. Eventually, we can leak this map object, revealing the pointer to our buffer. As the key is known we can use it to detect this object in the leak. By using the maximum allowed packet size, we have a couple of hundreds of bytes to store our ROP chain and payload.

This overall method is not perfectly reliable but works in 30%-50% of the cases. Even though the Bluetooth daemon is restarted automatically and forked by the same process. Therefore, the address space is only randomized on boot. Even if we crash the daemon, it is restarted with the same address layout, so an attacker can try over and over again to gain RCE.

Even though we know the absolute address of the, the offsets between the libraries are randomized as well. Therefore we only have gadgets available in that library.
The calls no interesting functions, such as system calls. At this point Fabian Beterke pointed out, that it would be a good idea to check the library imports for something interesting.

We have no direct use of system or execve, but we have dlsym available. This function requires a handle (e.g. NULL pointer) and a function name as an argument. It resolves and returns the address of that function and can be used to obtain the address to system. Therefore we need to perform a function call and return from it in a controlled way. In ROP, this is usually not a problem, as the gadgets have to end with a return anyway. However, we have no way to perform a stack pivot required for ROP. Thus, we have to use C++ object calls to perform the desired operations, which are often relative to X8 or X19. As a consequence, we have lots of relative references in our payload. To keep track of already used offsets, we implement a function called set_ptr( payload, offset, value), that will throw an error if a given offset in the payload is already used. We also keep track of the register values in order to simplify the process.

To return cleanly from dlsym, we used a deconstructor called u_cleanup_60. It iterates over a list of functions if a pointer is not NULL, the address is called and cleared. This is quite convenient, as we can call dlsym and can control the execution after the return, without a stack pivot.

ldr x8, [x19, #0x40]; cbz x8, #0xbc128; blr x8; str xzr, [x19, #0x40];
ldr x8, [x19, #0x48]; cbz x8, #0xbc138; blr x8; str xzr, [x19, #0x48];
ldr x8, [x19, #0x50]; cbz x8, #0xbc148; blr x8; str xzr, [x19, #0x50];
ldr x8, [x19, #0x58]; cbz x8, #0xbc158; blr x8;

This bug was initially sent to the Android Security Team and on November 3 2019, including a PoC. It was fixed on February 1, 2020, and acknowledged by the Android Security Team. I would like to thank the team to coordinate the process and providing a fix. Also, I would also like to thank Jiska Classen and Fabian Beterke for their assistance. Additionally, we want to give a shout-out to Swing’Blog and Marcin Kozlowski who was the first to our knowledge reversing the key idea of the vulnerability.

Scripts for testing can be downloaded here. The ROP chain has been removed from the exploit. The archive contains the following files:

  • python2 target PoC for the section “Unexpected leak”
  • python2 target PoC for the section “Leaking More Data”
  • python2 memcpy Unicorn emulation of memcpy for section “memwtf(,,-2);”
  • python2 target exploit excluding ROP chain

Leave a Reply

Your email address will not be published.