Bluetooth Security Research
BleedingTooth: Linux Bluetooth Zero-Click Remote Code Execution
Andy Nguyen (theflow@) - Information Security Engineer
BleedingTooth is a set of zero-click vulnerabilities in the Linux Bluetooth subsystem that can allow an unauthenticated remote attacker in short distance to execute arbitrary code with kernel privileges on vulnerable devices.
Table of Contents
Introduction
I noticed that the network subsystem was already being fuzzed extensively by syzkaller, but that subsystems like Bluetooth were less well covered. In general, research on the Bluetooth host attack surface seemed to be quite limited – with most public vulnerabilities in Bluetooth only affecting the firmware or the specification itself, and only allowing attackers to eavesdrop and/or manipulate information.
But what if attackers could take full control over devices? The most prominent examples that demonstrated this scenario were BlueBorne and BlueFrag. I set myself the goal to research the Linux Bluetooth stack, to extend upon BlueBorne’s findings, and to extend syzkaller with the capability to fuzz the /dev/vhci
device.
This blogpost describes the process of me diving into the code, uncovering high severity vulnerabilities, and ultimately chaining them into a fully-fledged RCE exploit targeting x86-64 Ubuntu 20.04.1 (video).
Patching, Severity and Advisories
Google reached out directly to BlueZ and the Linux Bluetooth Subsystem maintainers (Intel), rather than to the Linux Kernel Security team in order to coordinate the multi-party response for this series of vulnerabilities. Intel issued the security advisory INTEL-SA-00435 with the patches, but these weren’t included in any released Kernel versions at the time of disclosure. The Linux Kernel Security team should have been notified in order to facilitate coordination, and any future vulnerabilities of this type will also be reported to them. A timeline of the communications is at the bottom of this post. The patches for the respective vulnerabilities are:
Alone, the severity of these vulnerabilities vary from medium to high, but combined they represent a serious security risk. This write-up goes over these risks.
Vulnerabilities
Let’s briefly describe the Bluetooth stack. The Bluetooth chip communicates with the host (the operating system) using the HCI (Host Controller Interface) protocol. Common packets are:
Command packets – Sent by the host to the controller.
Event packets – Sent by the controller to the host to notify about events.
Data packets – Usually carry L2CAP (Logical Link Control and Adaptation protocol) packets, which implement the transport layer.
Higher-level protocols such as A2MP (AMP Manager Protocol) or SMP (Security Management Protocol) are built on top of L2CAP. In the Linux implementation, all these protocols are exposed without authentication, and vulnerabilities there are crucial since some of these protocols even live inside the kernel.
BadVibes: Heap-Based Buffer Overflow (CVE-2020-24490)
I discovered the first vulnerability (introduced in Linux kernel 4.19) by manually reviewing the HCI event packet parsers. HCI event packets are crafted and sent by the Bluetooth chip and usually cannot be controlled by attackers (unless they have control over the Bluetooth firmware as well). However, there are two very similar methods, hci_le_adv_report_evt()
and hci_le_ext_adv_report_evt()
, whose purposes are to parse advertisement reports coming from remote Bluetooth devices. These reports are variable in size.
Notice how both methods call process_adv_report()
, but the latter method does not check ev->length
to see if it is smaller or equal to HCI_MAX_AD_LENGTH=31
. The function process_adv_report()
then invokes store_pending_adv_report()
with the event data and length:
Finally, the store_pending_adv_report()
subroutine copies the data into d->last_adv_data
:
Looking at struct hci_dev
, we can see that the buffer last_adv_data
has the same size as HCI_MAX_AD_LENGTH
which is not enough to hold the extended advertising data. The parser can theoretically receive and route a packet up to 255 bytes to this method. If that is possible, we could overflow last_adv_data
and corrupt members up to offset 0xbaf.
However, is hci_le_ext_adv_report_evt()
even able to receive such a large report? It is likely that larger advertisements are anticipated, because it seems intentional that the extended advertisement parser explicitly removed the 31 bytes check. Also, since it is close to hci_le_adv_report_evt()
in code, that check has likely not been forgotten by mistake. Indeed, looking at the specification, we can see that extending from 31 bytes to 255 bytes is one of Bluetooth 5’s main features:
Recall in Bluetooth 4.0, the advertising payload was a maximum of 31 octets. In Bluetooth 5, we’ve increased the payload to 255 octets by adding additional advertising channels and new advertising PDUs. Source: https://www.bluetooth.com/blog/exploring-bluetooth5-whats-new-in-advertising/
Therefore, this vulnerability is only triggerable if the victim’s machine has a Bluetooth 5 chip (which is relatively “new” technology and only available on newer Laptops) and if the victim is actively scanning for advertisement data (i.e. open the Bluetooth settings and search for devices in the surrounding).
Using two Bluetooth 5-capable devices, we can easily confirm the vulnerability and observe a panic similar to:
The panic shows that we can take full control over members within struct hci_dev
. An interesting pointer to corrupt is mgmt_pending->next
, as it is of the type struct mgmt_pending_cmd
which contains the function pointer cmd_complete()
:
This handler can, for example, be triggered by aborting the HCI connection. However, in order to successfully redirect the mgmt_pending->next
pointer, we require an additional information leak vulnerability, as we will learn in the next section.
BadChoice: Stack-Based Information Leak (CVE-2020-12352)
The BadVibes vulnerability is not powerful enough to be turned into arbitrary R/W primitives, and there seems to be no way to use it to leak the memory layout of the victim. The reason is that the only interesting members that can be corrupted are pointers to circular lists. As the name suggests, these data structures are circular, thus we cannot alter them without ensuring that they eventually point back to where they started. This requirement is hard to fulfil when the memory layout of the victim is randomized. While there are some resources in the kernel that are allocated at static addresses, their contents are most likely not controllable. Therefore, we need to have an idea of the memory layout in the first place in order to exploit BadVibes. To be more concrete, we need to leak some memory addresses of the victim, whose content we can control or at least predict.
Usually, information leaks are achieved by exploiting out-of-bounds accesses, making use of uninitialized variables, or, as recently popular, by performing side-channel/timing attacks. The latter may be difficult to pull off, as transmissions may have jitter. Instead, let’s focus on the first two bug classes and go through all subroutines that send back some information to the attacker, and see if any of them can disclose out-of-bounds data or uninitialized memory.
I discovered the second vulnerability in the command A2MP_GETINFO_REQ
of the A2MP protocol by going through all a2mp_send()
invocations. The vulnerability has existed since Linux kernel 3.6 and is reachable if CONFIG_BT_HS=y
which used to be enabled by default.
Let’s take a look at the subroutine a2mp_getinfo_req()
invoked by the A2MP_GETINFO_REQ
command:
The subroutine is meant to request information about the AMP controller using the HCI device id. However, if it is invalid or not of the type HCI_AMP
, the error path is taken, meaning that the victim sends us back the status A2MP_STATUS_INVALID_CTRL_ID
. Unfortunately, the struct a2mp_info_rsp
consists of more members than just the id and the status, and as we can see, the response structure is not fully initialized. As a consequence, 16 bytes of kernel stack can be disclosed to the attacker which may contain sensitive data of the victim:
Such a vulnerability can be exploited by sending interesting commands to populate the stack frame prior to sending A2MP_GETINFO_REQ
. Here, interesting commands are those that put pointers in the same stack frame that a2mp_getinfo_req()
reuses. By doing so, uninitialized variables may end up containing pointers previously pushed onto the stack.
Note that kernels compiled with CONFIG_INIT_STACK_ALL_PATTERN=y
should not be vulnerable to such attacks. For example, on ChromeOS, BadChoice only returns 0xAA’s. However, this option does not seem to be enabled by default yet on popular Linux distros.
BadKarma: Heap-Based Type Confusion (CVE-2020-12351)
I discovered the third vulnerability while attempting to trigger BadChoice and confirm its exploitability. Namely, the victim’s machine unexpectedly crashed with the following call trace:
Taking a look at l2cap_data_rcv()
, we can see that sk_filter()
is invoked when ERTM (Enhanced Retransmission Mode) or streaming mode is used (similar to TCP):
This is indeed the case for the A2MP channel (channels can be compared with network ports):
Looking at amp_mgr_create()
, it is clear where the mistake is. Namely, chan->data
is of the type struct amp_mgr
, whereas sk_filter()
takes an argument of the type struct sock
, meaning that we have a remote type confusion by design. This confusion was introduced in Linux kernel 4.8 and since then has remained unchanged.
Exploitation
The BadChoice vulnerability can be chained with BadVibes as well as BadKarma to achieve RCE. In this blogpost, we will only focus on the method using BadKarma, for the following reasons:
It is not limited to Bluetooth 5.
It does not require the victim to be scanning.
It is possible to perform a targeted attack on a specific device.
The BadVibes attack, on the other hand, is a broadcast only, thus only one machine could be successfully exploited while all other machines listening to the same message would simply crash.
Bypassing BadKarma
Ironically, in order to exploit BadKarma, we must first get rid of BadKarma. Recall that there is a type confusion bug by design, and as long as the A2MP channel is configured as ERTM/streaming mode, we cannot reach the A2MP subroutines via l2cap_data_rcv()
without triggering the panic in sk_filter()
.
Looking at l2cap_data_channel()
, we can see that the only possible way to take a different route is to reconfigure the channel mode to L2CAP_MODE_BASIC
. This would “basically” allow us to invoke the A2MP receive handler directly:
However, is the reconfiguration of the channel mode even possible? According to the specification, the use of ERTM or streaming mode is mandatory for the A2MP channel:
The Bluetooth Core maintains a level of reliability for protocols and profiles above the Core by mandating the use of Enhanced Retransmission Mode or Streaming Mode for any L2CAP channel used over the AMP. Source: https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=421043
For some reason, this fact is not described in the specification and the implementation of Linux actually allows us to switch from any channel mode to L2CAP_MODE_BASIC
by encapsulating the desired channel mode in the L2CAP_CONF_UNACCEPT
configuration response:
This function invokes the subroutine l2cap_parse_conf_rsp()
. There, if the option type L2CAP_CONF_RFC
is specified, and the current channel mode is not L2CAP_MODE_BASIC
, it is possible to change it to our desire:
The natural question hereby is whether we first need to receive a configuration request from the victim before we can send back a configuration response? This seems to be a weakness of the protocol – the answer is no. Moreover, whatever the victim negotiates with us, we can send back a L2CAP_CONF_UNACCEPT
response and the victim will happily accept our suggestion.
Using the configuration response bypass, we are now able to reach the A2MP commands and exploit BadChoice to retrieve all the information we need (see later sections). Once we are ready to trigger the type confusion, we can simply recreate the A2MP channel by disconnecting and connecting the channel and as such, set the channel mode back to ERTM as required for BadKarma.
Exploring sk_filter()
As we understand, the issue of BadKarma is that a struct amp_mgr
object is passed to sk_filter()
, whereas a struct sock
object is expected. In other words, fields in struct sock
falsely map to fields in struct amp_mgr
. As a consequence, this could result in dereferencing invalid pointers and ultimately panic. Looking back at the panic log from before, this is exactly what happened and what primarily led to the discovery of BadKarma.
Can we control that pointer dereference, or control other members in struct amp_mgr
in order to affect the code-flow of sk_filter()
? Let’s take a look at sk_filter()
and track the usage of struct sock *sk
to understand what members are relevant in this subroutine.
The first usage of sk
is in sock_flag()
, though that function simply checks for some flags and moreover, only occurs if skb_pfmemalloc()
returns true. Instead, let’s take a look at BPF_CGROUP_RUN_PROG_INET_INGRESS()
and see what it does with the socket structure:
Similarly, sk_fullsock()
also checks for some flags and does not do anything interesting. Going further, note that sk->sk_family
must be either AF_INET=2
or AF_INET6=10
in order to continue. This field is located at offset 0x10 in struct sock
:
Looking at offset 0x10 in struct amp_mgr
, we realize that this field maps to the struct l2cap_conn
pointer:
As this is a pointer to a heap object which is aligned to the allocation size (minimum 32 bytes), it means that the lower bytes of this pointer cannot have the values 2 or 10 as required by __cgroup_bpf_run_filter_skb()
. Having established that, we know that the subroutine always returns 0 no matter what values the other fields have. Similarly, the subroutine security_sock_rcv_skb()
requires the same condition and returns 0 otherwise.
This leaves us with sk->sk_filter
as the only potential member to corrupt. We will later see how it may be useful to have control over struct sk_filter
, but first, note that sk_filter
is located at offset 0x110, whereas the size of struct amp_mgr
is only 112=0x70 bytes wide. Is it not out of our control then? Yes and no – usually it is not in our control, however if we have a way to shape the heap, then it may be even easier to take full control over the pointer. To elaborate, the struct amp_mgr
has a size of 112 bytes (between 65 and 128), thus it is allocated within the kmalloc-128 slab. Usually, memory blocks in the slab do not contain metadata such as chunk headers in front, as the goal is to minimize fragmentation. As such, memory blocks are consecutive and therefore, in order to control the pointer at offset 0x110, we must achieve a heap constellation where our desired pointer is located at offset 0x10 of the second block after struct amp_mgr
.
Finding a Heap Primitive
In order to shape the kmalloc-128 slab, we need a command that can allocate (preferably controllable) memory with a size between 65-128 bytes. Unlike other L2CAP implementations, the usage of the heap in the Linux implementation is quite low. A quick search for kmalloc()
or kzalloc()
in net/bluetooth/
yields nothing useful – or at least nothing that can be controlled or exist across multiple commands. What we would like to have is a primitive that can allocate memory of arbitrary size, copy attacker-controlled data into it, and leave it around until we decide to free it.
This sounds pretty much like kmemdup()
, right? Surprisingly, the A2MP protocol offers us exactly such a primitive. Namely, we can issue a A2MP_GETAMPASSOC_RSP
command to duplicate memory using kmemdup()
and store the memory address within a control structure:
In order for amp_ctrl_lookup()
to return a control structure, we must first add it into the list using the A2MP_GETINFO_RSP
command:
This is almost the perfect heap primitive, since the size and content can be arbitrary! The only downside is that there is no convenient primitive which allows us to free the allocations. It seems like the only way to free them is to close the HCI connection, which is a relatively slow operation. Yet, to understand how we may free allocations in a controlled way (e.g. free every second allocation to create holes), we need to pay close attention to the memory management. Note that when we store a new memory address at ctrl->assoc
, we do not free the memory block previously stored there. Rather, that memory block will simply be forgotten when we override it. To make use of this behavior, we can override every second ctrl->assoc
with an allocation of a different size, and once we close the HCI connection, the other half will be freed while the ones we overrode remain allocated.
Controlling the Out-Of-Bounds Read
So why did we want to have a heap primitive? Recall that the idea is to shape the heap and achieve a constellation where a memory block controlled by us is located one block away from the struct amp_mgr
object. By doing so, we can control the value at offset 0x110 which represents the sk_filter
pointer. As a result, when we trigger the type confusion, we can dereference an arbitrary pointer.
The following basic technique works quite reliably on Ubuntu which uses the SLUB allocator:
Allocate a lot of objects with size of 128 bytes to fill the kmalloc-128 slabs.
Create a new A2MP channel and hope that the
struct amp_mgr
object is adjacent to sprayed objects.Trigger the type confusion and achieve a controlled out-of-bounds read.
To verify that our heap spray was successful, we can first query /proc/slabinfo
for information about kmalloc-128 on the victim’s machine:
Then, after the heap spray, we can query once again and find that active_objs
increased:
In the example above, we sprayed 320 objects. Now, if we manage to allocate the struct amp_mgr
object in the surrounding of these newly sprayed objects, we may hit a panic trying to dereference a controlled pointer (observe the value of RAX):
Inspecting the memory address at RDI of the victim’s machine, we can see:
The value at 0xffff96da38f70410
shows that sk_filter()
indeed tried to dereference the pointer at offset 0x10 of our spray, which, from the perspective of struct amp_mgr
, is at offset 0x110. Bingo!
Leaking the Memory Layout
Now we have a way to shape the heap and prepare it for the BadKarma attack, and as such, have full control over the sk_filter
pointer. The question is, where shall we point it to? In order to make that primitive useful, we must point it to a memory address whose content we can control. That is where the BadChoice vulnerability comes into play. This vulnerability has the potential to disclose the memory layout and aid us in achieving the goal of controlling a memory block whose address we also know.
As mentioned earlier, in order to exploit uninitialized stack variable bugs, we must first send some different commands to populate the stack frame with interesting data (such as pointers to the heap or to .text segments relevant for ROP chains). Then, we can send the vulnerable command to receive that data.
By trying some random L2CAP commands, we can observe that by triggering BadChoice without any special command beforehand, a .text segment pointer to the kernel image can be leaked. Furthermore, by sending a L2CAP_CONF_RSP
and trying to reconfigure the A2MP channel to L2CAP_MODE_ERTM
beforehand, the address of a struct l2cap_chan
object at offset 0x110 can be leaked. This object has a size of 792 bytes and is allocated within the kmalloc-1024 slab.
It turns out that this object belongs to the A2MP channel and it can be deallocated by destroying the channel. This is useful because it allows us to apply the same strategy as for Use-After-Free attacks.
Consider the following technique:
Leak the address of the
struct l2cap_chan
object.Free the
struct l2cap_chan
object by destroying the A2MP channel.Reconnect the A2MP channel and spray the kmalloc-1024 slab with the heap primitive.
Possibly, it will reclaim the address of the former
struct l2cap_chan
object.
In other words, the address that belonged to struct l2cap_chan
may now belong to us! Again, the used technique is very basic but works quite reliably on Ubuntu with the SLUB allocator. A concern is that when reconnecting the A2MP channel, the former struct l2cap_chan
may be reoccupied by the new struct l2cap_chan
before the heap spray can reclaim the location. If that is the case, multiple connections can be used to have the ability to continue spraying even if the other connection has been shut down.
Note that allocating objects in the kmalloc-1024 slab is a bit more complicated than the kmalloc-128 slab, because:
The ACL MTU is usually smaller than 1024 bytes (can be checked with
hciconfig
).The default MTU for the A2MP channel is
L2CAP_A2MP_DEFAULT_MTU=670
bytes.
Both MTU limitations are easy to bypass. Namely, we can bypass the ACL MTU by fragmenting the request into multiple L2CAP packets, and we can bypass the A2MP MTU by sending a L2CAP_CONF_MTU
response and configuring it to 0xffff bytes. Here again, it is unclear why the Bluetooth specification does not explicitly disallow parsing configuration responses if no request has been sent.
Let’s try out the technique:
Notice how the most significant bytes of both leaked pointers differ. By observing the higher bytes, we can make an educated guess (or check the Linux documentation) to determine whether they belong to a segment, heap, or stack. To confirm that we were indeed able to reclaim the address of struct l2cap_chan
, we can inspect the memory on the victim’s machine using:
The memory content looks very promising! Note that it is useful to spray with a pattern, since that allows us to recognize memory blocks immediately and understand which offsets get dereferenced when a panic is hit.
Plugging It All Together
We now have all primitives we need to complete our RCE:
We can control a memory block whose address we know (referred to as the “payload”).
We can leak a .text segment pointer and build a ROP chain which we can store in the payload.
We can take full control over the
sk_filter
field and point it to our payload.
Achieving RIP Control
Let’s take a look back at sk_filter_trim_cap()
, and understand why having control over sk_filter
is beneficial.
Since we control the value of filter
, we can also control filter->prog
by placing a pointer at offset 0x18 in our payload. Namely, this is the offset of prog
:
Here, the structure of struct buf_prog
is:
The function bpf_prog_run_save_cb()
then passes filter->prog
to BPF_PROG_RUN()
:
That in turn calls bpf_dispatcher_nop_func()
with ctx
, prog->insnsi
and prog->bpf_func()
as parameters:
Finally, the dispatcher calls the prog->bpf_func()
handler with ctx
and prog->insnsi
as arguments:
All in all, we have:
As we have control over sk->sk_filter
, we also have control over the two later dereferences. This ultimately gives us RIP control with the RSI register (second argument) pointing to our payload.
Kernel Stack Pivoting
Since modern CPUs have NX, it is not possible to directly execute shellcodes. However, we can perform a code-reuse attack such as ROP/JOP. Of course, in order to reuse code, we must know where it is located, which is why the KASLR bypass is essential. Regarding the possible attacks, ROP is normally easier to perform than JOP, but that requires us to redirect the stack pointer RSP. For this reason, exploit developers usually perform JOP to stack pivot and then finish with a ROP chain.
The idea is to redirect the stack pointer to a fake stack in our payload consisting of ROP gadgets, i.e. our ROP chain. Since we know that RSI points to our payload, we want to move the value of RSI to RSP. Let’s see if there is a gadget that allows us to do so.
To extract the gadgets, we can use the following tools:
extract-vmlinux to decompress
/boot/vmlinuz
.ROPgadget to extract ROP gadgets from
vmlinux
.
Looking for gadgets like mov rsp, X ; ret
, we can see that none of them are useful.
Maybe there is something like push rsi ; pop rsp ; ret
?
Perfect, there are lots of gadgets that can be used. Interestingly, all gadgets dereference RBX+0x41, which is most likely part of a commonly used instruction or sequence of instructions. To elaborate, as instructions can begin at any byte in x86, they can be interpreted differently based on the start byte. The dereference of RBX+0x41 may actually hinder us from using the gadgets – namely, if RBX does not contain a writable memory address at the execution of bpf_func()
, we will simply hit a panic before we can execute our ROP chain. In our case, luckily, RBX points to the struct amp_mgr
object and it does not really hurt if the byte at offset 0x41 gets changed.
When choosing the stack pivot gadget as a function pointer for bpf_func()
and triggering it, the value of RSI will be pushed onto stack, then popped from stack and finally assigned to RSP. In other words, the stack pointer will point to our payload, and once the RET
instruction is executed, our ROP chain will kick off.
With that, we have finally achieved RCE. To debug our stack pivot and see if we were successful, we can set *(uint64_t *)&data[0x360]=0x41414141
and observe a controlled panic.
Kernel ROP Chain Execution
Now, we can either write a big ROP chain that retrieves and executes a C payload, or a smaller one that allows us to run an arbitrary command. For the sake of the Proof-Of-Concept, we are already satisfied with a reverse shell, thus executing a command is enough for us. Inspired by the ROP chain described in the write-up CVE-2019-18683: Exploiting a Linux kernel vulnerability in th