VENOM (CVE-2015-3456)
Virtualized Environment Neglected Operations Manipulation
Discovered by Jason Geffner
jason@crowdstrike.com
ABSTRACT
--------
This email describes 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").
SCOPE
-----
QEMU's virtualized hardware is used in numerous virtualization platforms, notably Xen, KVM, VirtualBox, and the native QEMU client.
VULNERABILITY
-------------
All code snippets in this section are from http://git.qemu.org/?p=qemu.git;f=hw/block/fdc.c;hb=24a5c62cfe3cbe3fb4722f79661b9900a2579316, which was the last commit before the VENOM vulnerability was 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[value & 0xff];
FLOPPY_DPRINTF("%s command\n", handlers[pos].name);
fdctrl->data_len = handlers[pos].parameters + 1;
fdctrl->msr |= FD_MSR_CMDBUSY;
}
FLOPPY_DPRINTF("%s: %02x\n", __func__, value);
fdctrl->fifo[fdctrl->data_pos++] = 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[fdctrl->fifo[0] & 0xff];
FLOPPY_DPRINTF("treat %s command\n", handlers[pos].name);
(*handlers[pos].handler)(fdctrl, handlers[pos].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[fdctrl->data_pos - 1] & 0x80) {
/* Command parameters done */
if (fdctrl->fifo[fdctrl->data_pos - 1] & 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.