sev1n75's Tech Blog

路漫漫其修远兮,吾将上下而求索

QEMU 学习笔记

| 0 | 前言

这是我学习 QEMU escape 的笔记,不是介绍 QEMU escape 的知识分享博客。我会在此博客分享学习 QEMU escape 的过程。

✅最近一次更新:2024-2-20


P.S. 因为截图有些麻烦,所以本文中的部分文字直接复制的原文

| 1 | ctf wiki

| 1.1 | 虚拟化基础知识

  • x86 CPU 虚拟化遇到的问题

    图 0

    敏感指令:该类指令即为操作特权资源的指令,例如 IO 操作、修改页表寄存器等。为了我们的 VMM 能够完全地控制系统资源,敏感质量必须在 VMM 的监控审查下完成,或是经由 VMM 来完成。

    特权指令:只有高特权的程序可以执行的指令。

    也就是说,当 Guest VM 运行非特权的敏感指令时,不会引发异常,因为他是非特权的,所以这时 VMM 无法介入。同时虚拟化结构要求敏感指令必须在 VMM 的监控审查下完成,或是经由 VMM 来完成,所以 x86 不是一个可以虚拟化的架构

  • Hypervisor

    是一个介于 VM 与硬件中间的软件层,其负责 VM 的创建、销毁等工作,并为 VM 提供了运行环境(VMM 是 Hypervisor 的一部分)。

  • KVM

    图 1 图 3
  • QEMU

    QEMU 是 Type II Hypervisor,运行在传统的操作系统上,与其他应用程序并行运行。

    QEMU-KVM 通过 KVM 来创建与运行虚拟机

    图 2

    总的来说,QEMU 即一个程序,如果控制了 QEMU 的程序流,就能实现 QEMU 逃逸,也就是通过劫持 QEMU 程序流使其开一个 shell

  • IO 虚拟化暂时跳过

| 1.2 | QEMU 基础知识

| 1.2.1 | 内存管理

  • Guest VM 视角(GPA)

    • MemoryRegion:Guest 视角的一块 “内存”

      MR 容器与 MR 实体间构成树形结构,其中容器为根节点而实体为子节点。

      MemoryRegion 的成员函数被封装在函数表 MemoryRegionOps 当中,当我们的 Guest 要读写虚拟机上的内存时,在 QEMU 内部实际上会调用 address_space_rw()

      • 对于一般的 RAM 内存而言则直接对 MR 对应的内存进行操作

      • 对于 MMIO 而言则最终调用到对应的 MR->ops->read()MR->ops->write()

      在 QEMU 当中 PMIO 的实现同样是通过 MemoryRegion 来完成的,我们可以把一组端口理解为 QEMU 视角的一块 Guest 内存。

      几乎所有的 CTF QEMU Pwn 题都是自定义一个设备并定义相应的 MMIO/PMIO 操作。

    • FlatView:MR 树对应的 Guest 视角物理地址空间

    • AddressSpace:不同类型的 Guest 地址空间

  • host VMM 视角(HVA)

    • RAMBlock:MR 对应的 Host 虚拟内存

| 1.2.2 | 设备模拟

  • QEMU 如何和设备 IO 交互

    图 4

    即我们在 VM 中访问虚拟设备的物理内存/端口,此时 QEMU 介入处理,在这种情况下我们可以劫持 QEMU 的程序流。

  • MMIO

    对于 MMIO 而言会调用到 address_space_rw() 函数,该函数会先将全局地址空间 address_space_memory 展开成 FlatView 后再调用对应的函数进行读写操作。

    操作函数最后会根据 FlatView 找到目标内存对应的 MemoryRegion,对于函数表中定义了读写指针的 MR 而言最后会调用对应的函数指针完成内存访问工作。

  • PMIO

    对于 PMIO 而言会调用到 kvm_handle_io() 函数,该函数实际上也是对 address_space_rw() 的封装,只不过使用的是端口地址空间 address_space_io,最后也会调用到对应 MemoryRegion 的函数表中的读写函数。

| 1.3 | QEMU 环境搭建-编写 QEMU 模拟设备

| 1.3.1 | QEMU Object Model

QOM 通过四个组件实现面向对象 Type, Class, Object, Propert,简单过程如下:

  • 编写 TypeInfo 这一结构体用来定义一个类的基本属性

  • 定义一个 Class 结构体来定义这个类的静态内容,包括函数表、静态成员等,其应当继承于对应的 Class 结构体类型

  • 定义一个相应的 Object 类型来表示一个实例对象,其包含有这个类实际的具体数据,且应当继承于对应的 Object 结构体类型

| 1.3.2 | QEMU 中 PCI 设备的编写

注意事项:

  • memory_region_init_io() 只限定读写 MR 的 addr 的大小,并不实际分配

其他的看 ctf-wiki 里 a3 写的教程就没问题

| 1.4 | QEMU 利用-交互方式

QEMU 题目主要与 PCI 设备交互,通过 lspci 查看 PCI 设备。lspci -v 可以获取 PCI 设备的编号以及 MMIO 地址(物理地址), PMIO 地址

lspci_info

也可以通过 sysfs 下的资源文件 resource 查看,例如:

1
cat /sys/devices/pci0000:00/0000:00:03.0/resource
  • PMIO

    可以先通过 iopl(3) 获取交互权限,接下来直接使用 in()out() 系函数(inb, inw, inl, outb, outw, outl)读写端口,需要注意的是端口地址应与读写长度对齐(读写长度即 MemoryRegionOpsvalidimpl 两个成员的值)

    例如下面例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int pmio_base = 0xc050;
    void pmio_write(uint32_t addr, uint32_t value)
    {
    outl(value, pmio_base + addr);
    }

    uint64_t pmio_read(uint32_t addr)
    {
    return inl(pmio_base + addr);
    }
    int main(int argc, char *argv[])
    {

    // Open and map I/O memory for the strng device
    if (iopl(3) !=0 ){
    perror("I/O permission is not enough");
    exit(-1);
    }
    }
  • MMIO

    MMIO 本质上是直接读写对应的物理地址,不过我们可以通过 mmap() 映射 sysfs 下的资源文件来完成内存访问,找到目标设备对应文件系统中 resource0 的路径,上面例子中对应路径为 /sys/devices/pci0000:00/0000:00:03.0/resource0

    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
    char* mmio_mem;

    void mmio_write(uint64_t addr, char value) {
    *(char*)(mmio_mem + addr) = value;
    }

    uint64_t mmio_read(uint64_t addr) {
    return *((char *)(mmio_mem + addr));
    }
    int main()
    {
    // open 设备对应的resource0
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0",O_RDWR | O_SYNC);
    //int fd2 = open("/dev/mem", O_RDWR | O_SYNC);
    if (mmio_fd == -1)
    {
    perror("mmio_fd open failed");
    exit(-1);
    }

    /* 映射外设I/O */
    mmio_mem = mmap(0,0x1000,PROT_READ | PROT_WRITE, MAP_SHARED,mmio_fd,0);
    if (mmio_mem == MAP_FAILED)
    {
    perror("mmap mmio_mem failed");
    exit(-1);
    }
    }

| 2 | PCI 设备简易食用手册

| 2.1 | PCI 概念简述

PCI 是一种总线标准,用于连接电脑主板和外部设备。

PCI 标准中的三个基本组件:

  • PCI 设备
  • PCI 总线
  • PCI 桥

| 2.2 | PCI 设备编号

PCI 设备编号的形式为 AAAA:BB:CC.D 的 16 进制编号,分别对应PCI域:总线编号:设备编号.功能编号

| 2.3 | PCI 设备配置空间

每个 PCI 逻辑设备中都有着其自己的配置空间(configuration space),通常是设备地址空间的前 64 字节(新版的设备还扩展了 0x40~0xFF 这段配置空间),其中存放了一些设备的基本信息,如生厂商信息、IRQ中断号、mem 空间与 io 空间的起始地址与大小等。

待补充

| 2.4 | PCI Base Address register

| 2.4.1 | 基本概念

PCI Base Address register(BAR) 是 PCI 设备配置空间的重要组成部分,用以定义 PCI 需要的配置空间大小以及配置 PCI 设备占用的地址空间

BAR 的格式有如下两种,对应 MMIO PMIO 两种 IO 方式:

图 0

| 2.4.2 | BAR 的初始化 & 处理器域与 PCI 域间访问

待补充

| 2.5 | PCI 设备内存 & 端口空间与访问方式

所有 IO 设备的内存与端口空间需要被映射到对应的地址空间/端口空间中才能访问。

有两种映射外设资源的方式:

  • MMIO(Memory-mapped I/O)

  • PMIO(Port-mapped I/O)

这种方式将 IO 设备的寄存器编码到指定的端口上,我们需要通过访问端口的方式来访问设备的寄存器与内存(例如在 x86 下通过 in 与 out 这一类的指令可以读写端口)。IO 设备通过专用的针脚或者专用的总线与 CPU 连接,这与内存地址空间相独立,因此又称作 isolated I/O。

P.S. IO 设备的端口是指用于与 CPU 进行输入输出通信的特定地址。这些端口通常被称为 IO 端口或 IO 地址空间。

IO 设备的端口是一种与内存地址分开的地址空间,它专门用于与外部设备进行通信。

每个 IO 设备通常会占用一组连续的端口地址,其中不同的端口用于不同的设备寄存器或功能。

| 2.6 | PCI 中断机制

待补充

| 3 | 调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> ptype /ox MariaState
type = struct {
/* 0x0000 | 0x0a20 */ PCIDevice pdev;
/* 0x0a20 | 0x0010 */ struct {
/* 0x0a20 | 0x0008 */ uint64_t src;
/* 0x0a28 | 0x0001 */ uint8_t off;
/* XXX 7-byte padding */

/* total size (bytes): 16 */
} state;
/* 0x0a30 | 0x2000 */ char buff[8192];
/* 0x2a30 | 0x0110 */ MemoryRegion mmio;

/* total size (bytes): 11072 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> set $maria = (MariaState *)0x55e912ae2770
pwndbg> p $maria->mmio
$23 = {
parent_obj = {
class = 0x55e911b8eae0,
free = 0x0,
properties = 0x55e912ae6fa0,
ref = 1,
parent = 0x55e912ae2770
},
romd_mode = true,
ram = false,
subpage = false,
readonly = false,
nonvolatile = false,
rom_device = false,
flush_coalesced_mmio = false,
dirty_log_mask = 0 '\000',
is_iommu = false,
ram_block = 0x0,
owner = 0x55e912ae2770,
ops = 0x55e90f6ddf80 <maria_mmio_ops>,
opaque = 0x55e912ae2770,
//....

| 4 | 题目