| 0 | 前言
这是我学习 QEMU escape 的笔记,不是介绍 QEMU escape 的知识分享博客。我会在此博客分享学习 QEMU escape 的过程。
✅最近一次更新:2024-2-20
P.S. 因为截图有些麻烦,所以本文中的部分文字直接复制的原文
| 1 | ctf wiki
| 1.1 | 虚拟化基础知识
x86 CPU 虚拟化遇到的问题
敏感指令:该类指令即为操作特权资源的指令,例如 IO 操作、修改页表寄存器等。为了我们的 VMM 能够完全地控制系统资源,敏感质量必须在 VMM 的监控审查下完成,或是经由 VMM 来完成。
特权指令:只有高特权的程序可以执行的指令。
也就是说,当 Guest VM 运行非特权的敏感指令时,不会引发异常,因为他是非特权的,所以这时 VMM 无法介入。同时虚拟化结构要求敏感指令必须在 VMM 的监控审查下完成,或是经由 VMM 来完成,所以 x86 不是一个可以虚拟化的架构
Hypervisor
是一个介于 VM 与硬件中间的软件层,其负责 VM 的创建、销毁等工作,并为 VM 提供了运行环境(VMM 是 Hypervisor 的一部分)。
KVM
QEMU
QEMU 是 Type II Hypervisor,运行在传统的操作系统上,与其他应用程序并行运行。
QEMU-KVM 通过 KVM 来创建与运行虚拟机
总的来说,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 交互
即我们在 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 地址
也可以通过 sysfs 下的资源文件 resource
查看,例如:
1 | cat /sys/devices/pci0000:00/0000:00:03.0/resource |
PMIO
可以先通过
iopl(3)
获取交互权限,接下来直接使用in()
与out()
系函数(inb, inw, inl, outb, outw, outl
)读写端口,需要注意的是端口地址应与读写长度对齐(读写长度即MemoryRegionOps
的valid
和impl
两个成员的值)例如下面例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int 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
28char* 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 方式:
| 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 | pwndbg> ptype /ox MariaState |
1 | pwndbg> set $maria = (MariaState *)0x55e912ae2770 |