HubCap: pwning the ChromeCast pt. 1
In case you’re looking for the root, it was released a little while back: here
The foothold
I’d tell you all about what the Chromecast is, but I think Wikipedia has that part covered for me. For our purposes, all you need to know is that it’s an ARMv7 based device, has WiFi, an HDMI connector and a maintenance port in the form of a micro USB port. Additionally you can crack the case open and get access to a serial debugging port. It runs a stripped down version of Android without the Dalvik engine.
The Chromecast processor supports “secure” boot, which means that it will only boot a bootloader and kernel authorized by Google. But what if I want to run my own kernel? Well, that’s where our story starts. The Chromecast is a little bit unique in that Google’s released the source code to most of the system, including the second stage bootloader. This bootloader is a forked version of U-Boot maintained by Marvell, the SoC manufacturer. It was modified in order to boot Android images.
Because we have access to the source code, we can simply audit it. There’s only really one way of getting data into the bootloader without having to crack the case open and that’s the maintenance port. This port is configured as a USB host in this scenario so let’s have a look at the code responsible for handling device insertions. In the function responsible for parsing the USB configuration descriptors, we find this:
head = (struct usb_descriptor_header *) &buffer[0];
if (head->bDescriptorType != USB_DT_CONFIG) {
printf(" ERROR: NOT USB_CONFIG_DESC %x\n",
head->bDescriptorType);
return -1;
}
memcpy(&dev->config, buffer, buffer[0]);
le16_to_cpu(&(dev->config.desc.wTotalLength));
dev->config.no_of_if = 0;
index = dev->config.desc.bLength;
/* Ok the first entry must be a configuration entry,
* now process the others */
head = (struct usb_descriptor_header *) &buffer[index];
while (index + 1 < dev->config.desc.wTotalLength) {
switch (head->bDescriptorType) {
case USB_DT_INTERFACE:
if (((struct usb_interface_descriptor *) \
&buffer[index])->bInterfaceNumber != curr_if_num) {
/* this is a new interface, copy new desc */
ifno = dev->config.no_of_if;
dev->config.no_of_if++;
memcpy(&dev->config.if_desc[ifno],
&buffer[index], buffer[index]);
dev->config.if_desc[ifno].no_of_ep = 0;
dev->config.if_desc[ifno].num_altsetting = 1;
curr_if_num =
dev->config.if_desc[ifno].desc.bInterfaceNumber;
} else {
/* found alternate setting for the interface */
dev->config.if_desc[ifno].num_altsetting++;
}
break;
The memcpy calls shown here on lines 7 and 23 are only mildly bounded (255 bytes) invocations. But more importantly, the ‘ifno’ variable - responsible for counting the number of interface descriptors - isn’t bounded either. You can supply as many interface headers as you want, and the code won’t care. It’s looking like we can overflow a decent amount outside this ‘dev’ struct. So we plug in a USB device with a malformed configuration descriptor and we win, right? Well… Not quite. The allocations for these dev structs are being done in usb_alloc_new_device:
struct usb_device *usb_alloc_new_device(void)
{
int i;
USB_PRINTF("New Device %d\n", dev_index);
if (dev_index == USB_MAX_DEVICE) {
printf("ERROR, too many USB Devices, max=%d\n", USB_MAX_DEVICE);
return NULL;
}
/* default Address is 0, real addresses start with 1 */
usb_dev[dev_index].devnum = dev_index + 1;
usb_dev[dev_index].maxchild = 0;
for (i = 0; i = USB_MAXCHILDREN; i++)
usb_dev[dev_index].children[i] = NULL;
usb_dev[dev_index].parent = NULL;
dev_index++;
return &usb_dev[dev_index - 1];
}
Each entry in usb_dev is about 1300 bytes large, and USB_MAX_DEVICE is 32. That means this array is about 42k large. We’re only the second device to get inserted (the first being the root hub), so there’s about 40k of unused memory next to us. We can’t overflow that much memory! We’re going to have to figure out a way of increasing dev_index until we can hit something interesting.
The hardware
Introducing the hardware we’ll use to attack this platform, the Teensy 2.0++. I used this handy stick partly because I had it around, partly because I’m already familiar with its environment and I’ve done USB exploitation with it before. We’re going to abuse the Teensy’s flexibility to increase the device_index to a point where we can hit something outside of this array.
We’ll simply have to insert a lot of ‘fake’ devices. We can do this by pretending to be a USB hub, and inserting lots of devices. There’s some trickery involved here because we have to be both the hub and lots of devices at the same time, but the Teensy can only listen to one USB address at a time. See the code for how exactly this is done. So we’ve now got a device inserted into the last slot. We win, right? Not quite. Unlike a more traditional platform, bootloaders are special. There’s usually no heap, no dynamic allocations and the structures are all very simple. Now what? We can’t corrupt heap metadata, and there’s no function pointers nearby. Let’s look at what we do have nearby:
RAM:006E2FDC hub_index DCD
RAM:006E2FE8 dev_array DCD
...
RAM:006ED8E8 dev_index DCD
RAM:006ED8EC hub_array DCD
As you can see, right after the dev_array is the dev_index variable that was used in the usb_alloc_new_device function. We can modify this 32 bit value and get the next ‘allocation’ of a USB device to point anywhere! So, now we win, right? Still no. If you look at the above code, you notice that it also uses the dev_index to assign the devices ‘devnum’ or address. USB addresses run from 0 to 127. The code handles anything else rather ungracefully and fails to set up the USB EHCI controller correctly.
We can set dev_index to be anything from -1, 126 then. The array is 32 entries long so 32-126 will overwrite memory after the array, -1 will allow us an overwrite before the array. There’s nothing interesting in memory after the usb_dev array outside of the dev_index (it got stuffed at the end of the memory space). That leaves us with writing to the memory in front of the usb_dev array. It so happens that this piece of memory contains something that IS interesting.
The bootloader has support for two kinds of devices, USB hubs and USB Mass Storage devices. It has arrays similar to our usb_dev array to keep track of the device specific data associated with these devices. Also similar to the device array is that it uses indexes to keep track of these devices. One of these indexes (for the hubs) lives in front of the usb_dev array and thus we can overwrite it! However, looking at the hub allocation code, it’s a little bit more strict:
static int usb_hub_index;
if (usb_hub_index < USB_MAX_HUB)
return &hub_dev[usb_hub_index++];
It checks that usb_hub_index is less than USB_MAX_HUB (16). But it does this using a signed integer so if we supply a negative usb_hub_index (something that doesn’t happen during normal codeflow) we can write anywhere in front of the hub_dev array! Reading the linker script the bootloader uses, we get the following memory map:
Offset 0 +----------+
| |
| text |
| |
+----------+
| |
| rodata |
| |
+----------+
| |
| data |
| |
+----------+
| |
| bss |
| |
+----------+
| |
| stack |
| |
Top of mem +----------+
The .text segment is in front of the .data segment, and the pagetables are set up to allow read,write and execute accesses to all the pages. We can just overwrite the .text segment, and we win, right? I hate to disappoint, but no. There’s more than one problem with this approach. I’ll start with the first: we don’t know where we are in memory. All we have is a relative index from an array ‘somewhere’ in memory. Plus, we don’t know what .text looks like. Up to this point, I’ve been working with source code for the current version, and an older firmware dump graciously provided by our friends over at GTVHacker. But I don’t have a firmware dump for the version of the bootloader on my chromecast. So, even if we could overwrite .text (more on that later), we wouldn’t know what to overwrite where.
We can solve this problem, but that’s for Part II, the overflowening. (I need to work on my titles).