Recently, I discovered a vulnerability in QEMU's virtual Floppy Disk Controller (FDC), exploitation of which may allow malicious code inside a virtual machine guest to perform arbitrary code execution on the host machine (a "VM escape"). Since then, my colleagues and I have worked closely with key impacted software vendors, major infrastructure service providers, and security appliance vendors to remediate the vulnerability prior to or immediately upon the May 13th, 2015 public disclosure and patch release. With fixes now released and the impact of the vulnerability minimized through the industry’s patching practices, I wanted to follow up with the release of the full details of the critical-impact vulnerability VENOM (CVE-2015-3456) for a technical audience. All code snippets below are from http://git.qemu.org/?p=qemu.git;f=hw/block/fdc.c;hb=24a5c62cfe3cbe3fb4722f79661b9900a2579316, which was the latest version of QEMU's FDC code before the VENOM vulnerability was publicly patched on 2015-05-13. QEMU's FDC uses a 512-byte first-in first-out (FIFO) buffer for data storage:
struct FDCtrl { ... /* Command FIFO */ uint8_t *fifo; int32_t fifo_size; uint32_t data_pos; uint32_t data_len; ... }; ... fdctrl->fifo = qemu_memalign(512, FD_SECTOR_LEN); fdctrl->fifo_size = 512;
The FDC's data_pos and data_len fields above are initialized to 0 upon FDC reset. Code in the VM guest can write to the FIFO buffer by sending data to the FDC via its FD_REG_FIFO I/O port. Writes are handled by the function below, with each byte sent to the I/O port getting passed to this function as the value parameter:
static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value) { FDrive *cur_drv; int pos; ... if (fdctrl->data_pos == 0) { /* Command */ pos = command_to_handler; FLOPPY_DPRINTF("%s command\n", handlers.name); fdctrl->data_len = handlers.parameters + 1; fdctrl->msr |= FD_MSR_CMDBUSY; } FLOPPY_DPRINTF("%s: %02x\n", __func__, value); fdctrl->fifo = value; if (fdctrl->data_pos == fdctrl->data_len) { /* We now have all parameters * and will be able to treat the command */ if (fdctrl->data_state & FD_STATE_FORMAT) { fdctrl_format_sector(fdctrl); return; } pos = command_to_handler & 0xff>; FLOPPY_DPRINTF("treat %s command\n", handlers.name); (*handlers.handler)(fdctrl, handlers.direction); } }
The function above uses the first received I/O byte as a command ID, with each ID mapping to a handler:
static const struct { uint8_t value; uint8_t mask; const char* name; int parameters; void (*handler)(FDCtrl *fdctrl, int direction); int direction; } handlers<> = { { FD_CMD_READ, 0x1f, "READ", 8, fdctrl_start_transfer, FD_DIR_READ }, { FD_CMD_WRITE, 0x3f, "WRITE", 8, fdctrl_start_transfer, FD_DIR_WRITE }, { FD_CMD_SEEK, 0xff, "SEEK", 2, fdctrl_handle_seek }, ... };
Each handler has an associated parameter count (stored in the parameters variable) and the I/O bytes written to the FIFO buffer after the command ID byte are considered parameters for the command handler. When fdctrl_write_data() determines that all parameters have been supplied (by comparing the incrementing data_pos with data_len), the command handler's function is called to operate on the data in the FIFO buffer. The FDC supports 32 different FIFO-based commands, including a default handler for unrecognized command ID values. The code path for each command handler function resets the FDC's data_pos to 0 at the end of its processing, ensuring that the FIFO buffer can't be overflowed. See below for an example, where the handler function fdctrl_handle_partid() calls fdctrl_set_fifo() which resets data_pos to 0:
{ FD_CMD_PART_ID, 0xff, "PART ID", 0, fdctrl_handle_partid },
static void fdctrl_handle_partid(FDCtrl *fdctrl, int direction) { fdctrl->fifo<0> = 0x41; /* Stepping 1 */ fdctrl_set_fifo(fdctrl, 1); }
static void fdctrl_set_fifo(FDCtrl *fdctrl, int fifo_len) { fdctrl->data_dir = FD_DIR_READ; fdctrl->data_len = fifo_len; fdctrl->data_pos = 0; fdctrl->msr |= FD_MSR_CMDBUSY | FD_MSR_RQM | FD_MSR_DIO; }
For 30 of the command handler functions, this data_pos reset happens immediately at the completion of the command processing, similarly to the example above. However, for two of the command handler functions, the data_pos reset is delayed or can be circumvented. The code below shows the handler for the "READ ID" command:
{ FD_CMD_READ_ID, 0xbf, "READ ID", 1, fdctrl_handle_readid },
static void fdctrl_handle_readid(FDCtrl *fdctrl, int direction) { FDrive *cur_drv = get_cur_drv(fdctrl); cur_drv->head = (fdctrl->fifo<1> >> 2) & 1; timer_mod(fdctrl->result_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + (get_ticks_per_sec() / 50)); }
The fdctrl_handle_readid() function above sets a 20 ms timer (result_timer is initialized to point to the function fdctrl_result_timer() during FDC initialization). After 20 ms, fdctrl_result_timer() gets executed, which calls fdctrl_stop_transfer(), which calls fdctrl_set_fifo(), which resets data_pos to 0. During that 20 ms time window though, code in the VM guest can continue writing to the FIFO buffer, and since fdctrl_write_data() will continue to increment data_pos for each I/O byte received, code in the VM guest can overflow the FIFO buffer with arbitrary data. The code below shows the handler for the "DRIVE SPECIFICATION COMMAND" command:
{ FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, "DRIVE SPECIFICATION COMMAND", 5, fdctrl_handle_drive_specification_command },
static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction) { FDrive *cur_drv = get_cur_drv(fdctrl); if (fdctrl->fifo & 0x80) { /* Command parameters done */ if (fdctrl->fifo & 0x40) { fdctrl->fifo<0> = fdctrl->fifo<1>; fdctrl->fifo<2> = 0; fdctrl->fifo<3> = 0; fdctrl_set_fifo(fdctrl, 4); } else { fdctrl_reset_fifo(fdctrl); } } else if (fdctrl->data_len > 7) { /* ERROR */ fdctrl->fifo<0> = 0x80 | (cur_drv->head << 2) | GET_CUR_DRV(fdctrl); fdctrl_set_fifo(fdctrl, 1); } }
The fdctrl_handle_drive_specification_command() function above is called after the FDC receives the FD_CMD_DRIVE_SPECIFICATION_COMMAND command and its 5 parameter bytes. The if-condition in the handler will evaluate to false if the fifth parameter byte doesn't have its most-significant-bit (MSB) set. The else-if-condition will always evaluate to false since fdctrl->data_len will always be set to 6 by the fdctrl_write_data() function for the FD_CMD_DRIVE_SPECIFICATION_COMMAND command. Thus, after sending the FD_CMD_DRIVE_SPECIFICATION_COMMAND command and its 5 parameter bytes (with the fifth parameter byte's MSB set to 0), code in the VM guest can continue writing to the FIFO buffer, and since fdctrl_write_data() will continue to increment data_pos for each I/O byte received, code in the VM guest can overflow the FIFO buffer with arbitrary data.