生活,总是在理所当然的挫折与不期而遇的惊喜之间变得层次分明。
This was my first time to finish a “Real-world” style challenge in CTF game. Having so much fun on this Qemu escape challenge, I learned a lot about how Linux drivers work with the low-level devices when solving this challenge. Our team stands on the fifth place at the end. See you at Shenzhen by the end of 2019!
You can download the challenge zip file from this link: vexx.zip. The login username is root
, and password is goodluck
. If you want to learn virualization related pwning topic, I believe this will be a good starting point.
Recon
The structure of the provided archive file looks like this:
1 | . |
Let’s see what launch.sh
tells us:
1 |
|
I can tell two suspicious terms at the first glaze, the first one is -L ./pc-bios
, it tells us this is used to set the directory for the BIOS, VGA BIOS and keymaps in --help
command. The second unusual term is -device vexx
, it seems like the designer implemented a custom device, so this is very likely the correct place to look for vulnerabilities.
After running launch.sh
and login as it suggested. it gives us a root shell.
1 | Welcome to VM world! |
So this challenge assumes we have full control of the guest system and our target is reading the flag file from the host machine. My teammate suggested some Qemu escape writup from previous CTFs: HITB GSEC 2017: babyqemu, Hitb 2017 - Babyqemu. The writeups pointed out that the custom devices code is located in the emulator binary file. The provided emulator is version 4.0.0
, so I downloaded the corresponding source and compiled it. Then I feed them to bindiff
and see the modifications. Sorting the functions by difference, soon we can see a function called vexx_mmio_write
. Well, seems this is it, if we search the string “vexx” in function window, there are more related functions.
As suggested by previous writeups, first we can easily found the device state structure in Local Types window.
1 | struct VexxState |
and its related strutures:
1 | struct VexxRequest |
Thanks to the debug symbols, these look pretty readable. Then we can start investing the code from vexx_class_init
:
1 | void __fastcall vexx_class_init(ObjectClass_0 *a1, void *data) |
This function specified the vendor_id
at line 18, and it registered the realize function and exit function. So we can follow up to realize function pci_vexx_realize
:
1 | void __fastcall pci_vexx_realize(PCIDevice *pdev, Error_0 **errp) |
From line 2630 is what really matters here. Line 26 and 27 share the same pattern, in short,it initializes two memory region for memory-mapped I/O, and bind corresponding operations functions to them. Line 28\30 initialize another I/O method. It defines a range of I/O port numbers for specified operations.
Vulnerabilites
cmb_mmio
From previous chapter we figure out what operations the devices provided to interact with. There are two MMIO region call vexx-mmio
and vexx-cmb
, and a series of I/O ports from 0x230. Each I/O channel is bound to read and write methods. Let’s start from vexx_cmb_read
and vexx_cmb_write
:
1 | int64_t __fastcall vexx_cmb_read(VexxState *state, hwaddr addr, unsigned int size) |
1 | void __fastcall vexx_cmb_write(VexxState *state, hwaddr addr, uint64_t val, unsigned int size) |
The code is neat. These functions basically perform R/W operations on state->req.req_buf
, which is a buffer in size of 0x100. I notice that if we can control state->memorymode
to 1 and state->req.offset
to non-zero value, so we can hit line 11 and perform out-of-bound read/write.
Following this idea, we can find these varibles were initialized in vexx_instance_init
:
1 | void __fastcall vexx_instance_init(__int64 obj) |
Port I/O
Soon I found that we can control those varialbes through port I/O.
1 | void __fastcall vexx_ioport_write(VexxState *opaque, uint32_t addr, uint32_t val) |
Well, the idea is pretty clear now, we can combine these two I/O operations and perform OOB R/W on VexxState
object. Right after state->req.reqbuf
is an VexxDma
object called vexxdma
.
Debugging setup
Remeber that our attack target is Qemu emulator itself, so we can simply attach gdb to the running process, like sudo gdb attach (pidof qemu-system-x86_64)
. And we need to copy the exp into the guest machine. My solution here is to mount the provided ext2 file and copy our exp into it, then lauch the virtual machine again.
1 | sudo mount ./rootfs.ext2 ./rootfs -t ext2 |
Exploitation
Talking to mmio
The first step is to find a way to talking to the device and make it invokes vulnerable functions. Hitb 2017 - Babyqemu shows how to locate the mmio file in details.
As for this challenge, we can identify two mmio file according to the size.
1 | -rw------- 1 root root 16384 Aug 24 02:13 /sys/devices/pci0000:00/0000:00:04.0/resource1 [vexx-cmb] |
Then we can setup the mmio from userspace:
1 | int fdcmb = open("/sys/devices/pci0000:00/0000:00:04.0/resource1", O_RDWR|O_SYNC); |
If we setup a breakpoint at vexx_cmb_write
, and copy some data to cmb
, we can see the breakpoint is hitted.
Talking to port I/O
The previous writeups didn’t cover the knowledge about how to talk to the port I/O. So I spent some time to figure out it by myself. Using I/O ports in C programs introduce that we can access the I/O port by inb(port)
and outb(value, port)
. Before that, we also need to setup the permission by ioperm()
.
The question for us now is what is the io number? From pci_vexx_realize
, it gives us some hits like:
1 | portio_list_init(&state->port_list, &state->pdev.qdev.parent_obj, vexx_port_list, state, "vexx"); |
It seems that it start from 0x230, and vexx_ioport_write
is where we can comfirm our guess:
1 | void __fastcall vexx_ioport_write(VexxState *opaque, uint32_t addr, uint32_t val) |
Port number 0x230 for accessing memorymode
, and 0x240 for req.offset
. That all we need to perform a OOB R/W.
Leaking & Hijacking
With the OOB ability, we need to comfirm its impact, that is, take a look at what we can leak or overwrite from the object. We can set a breakpoint at veex_cmb_mmio
, and see how the VeexState
object looks like using the command p *(VexxState *) $rdi
,as $rdi
points to the object itself now.
1 | pwndbg> p *(VexxState *) $rdi |
Once printed out the structure, it becomes very clear. We can leak the code address from dma_timer.cb
, it points to the address of vexx_dma_timer
, and there is a heap address at dma_timer.opaque
, it happens to point to the VexxState
object itself.
Of course we can overwrite these two pointers, the dma_timer.cb
seems will be triggered by some kind of timing mechanism. After some investigation, I found that the cb
function can be triggered indirectly by a special command in vexx_mmio_write
:
1 | void __fastcall vexx_mmio_write(VexxState *vexx, hwaddr addr, uint64_t val, unsigned int size) |
The vexx_mmio_write
will register an event to the timer instead of writing the value right away. And when the time’s up, it will call dma_timer.cb
, the first argument is dma_timer.opaque
. Therefore, we can use OOB to write dma_timer.cb
to system@plt
, and dma_time.opaque
points to "/bin/sh"
. When the timer is triggered, it can give us a shell! Well, after some trying system("/bin/sh");
does not work as the emulator just hang there, but we can instead use system("cat flag")
to obtain the glory flag!
Exp
1 |
|
Wrapup
This challenge is not as difficult as it looks. Qemu escape sounds scary, but the vulnerability logic is simple and exploitation is kind of straight-forward. Just require some patient and confident! From this challenge, I learned how to access the low-level devices from the userspace with mmio or port-io. I think building the exploit from these communication channels is the general pattern of VM-escape style CTF challenge.