qemu逃逸这部分内容,在今年年初就有所涉猎,然而由于本人过于懒惰,这个部分的总结推了整整半年多。直到参加本月的HTICON2023,真正第一次在比赛中面对QEMU逃逸,发现了自己很多的不足之处。借这次机会将这部分内容作以总结。
PCI设备
在目前的QEMU逃逸题目类型中,出题者一般会在qemu-system-x86_64
(或是对应arm架构的qemu-system)的二进制程序中加入自己编写的、具有漏洞的PCI设备模块。每个PCI设备都对应一个PCI配置空间,包含该PCI设备的相关信息,结构如图。
针对PCI设备,其对应内存空间有两种形式,MMIO和PMIO
MMIO 内存映射I/O
MMIO全称位Memory mapping I/O,即内存映射I/O. 直观来讲,就是内存和I/O共享一个地址空间。在计算机组成原理中,我们也接触过这样的形式:直接将一部分内存地址空间分配于外设,访问外设和代码数据使用相同的地址总线。
因此访问这种外设的I/O非常方便,因为在同一地址空间下,我们只需要直接访问映射出来的虚拟地址空间即可,对于MMIO型设备,我们可以用如下的方式访问
1 | char* mmio_mem; |
PMIO 端口映射I/O
PMIO全称为Port-mapped I/O,即端口映射I/O,也被称为隔离的I/O(isolated I/O)。在这种外设内存分配形式下,外设拥有独立的地址空间,因而在物理接口层面一般会有独立的I/O总线或I/O引脚来对这种外设进行访问。
在这种情况下,端口映射I/O需要特殊的指令来进行I/O操作。在intel微处理器下,一般使用IN和OUT来进行I/O操作。例如:inb, inw, inl, outb, outw, outl等,用于读写不同长度,b代表byte,w代表word,l代表long。访问方式如下
1 | int pmio_base = 0xc040; |
查看系统PCI设备
主要可以使用两个命令lspci
和info pci
lspci
在linux环境下我们可以使用命令lspci
来列出该系统装载的的所有pci设备
1 | /root # lspci |
以第5行00:05.0 Class 00ff: 1234:dead
为例,来介绍每个部分含义,从左向右具体内容指代如下:
00
代表总线标号05.0
,其中05
代表设备号,.0
用来表示功能号00ff
,class_id1234
,vendor_iddead
,device_id
一般通过后三个内容信息,我们便可知道要找的PCI设备对应哪一个内容.
info pci
在没用lspci命令的情况下,我们也可以使用info pci
命令查看,不过其为QEMU的monitor所包含的指令。
首先要修改run.sh
,在结尾部分添加monitor选项-monitor telnet:127.0.0.1:4444,server,nowait
1 |
|
之后我们便可以使用nc
或者telnet
进行连接
1 | ┌─[loorain@ubuntu] - [~/PWN/hitcon/2023/wall_maria/share] - [5120] |
可以看到,其给出了更加详细PCI设备内容
调试QEMU
QEMU逃逸本质上可以回归为用户态的漏洞利用问题,以为实际情况便是劫持在宿主机上运行的qemu-system
程序实现宿主机RCE,之后通过反弹shell或者其他操作便完成了一个QEMU逃逸。
因此调试QEMU的方式,便是直接attach到qemu-system
进程,具体流程如下
首先找到qemu-system
的进程号,可以使用ps -aux | grep "qemu-system"
1 | ┌─[loorain@ubuntu] - [~/PWN/hitcon/2023/wall_maria/share] - [5128] |
然后需要用root权限直接gdb qemu-system,之后attach到该进程
1 | ┌─[loorain@ubuntu] - [~/PWN/hitcon/2023/wall_maria/share] - [5132] |
剩下就是下断点以及调试了
远程exp脚本
这部分偷的wjh师傅的exp模板,Kernel和QEMU逃逸类型题目都是通用的,也可以更具自己需求自定义内容。
根据我自己的经验,有时候gcc生成的静态链接程序过于庞大,pwntools可能会出现传输卡死的情况发生。这个时候可以考虑用musl-gcc静态编译exp文件,体积一般能直接缩小到1/3的大小,再配合gzip压缩,传输效果嘎嘎好👌
上传脚本1(一次性发送)
1 | #!/usr/bin/env python |
上传脚本2(分段发送)
1 | #coding:utf8 |
实战练习
这里我选用的题目是HITCON2023 wall-maria,算是比赛pwn方向的签到题,难度比较适合做qemu入门
改题目甚至直接给了maria设备的相关源码,最大程度降低了题目难度,不过我们还是从逆向开始分析,尽可能介绍这类题目的完整流程
逆向分析
直接将给予我们的qemu-system-x86_64
拖入IDA中,要找到我们需要的PCI设备相关函数,首先要知道设备名称。一般从启动脚本或是题目名称中可以得到相关信息,比如本题的启动脚本中包含-device maria
我们便可以在IDA的函数中搜索关键词maria
,可以得到如下结果
函数名称一般就能够直观表示功能,比如使用MMIO进行读写操作的函数,还有初始化函数相关的内容。
maria_class_init
首先我们来看看初始化相关的函数,进入maria_class_init
,可以看到如下内容
这里比较明显的设置了该设备的一些信息,比如vendor_id
, device_id
, class_id
等等。对应lspci
得到的内容我们可以确定设备对应00:05.0 Class 00ff: 1234:dead
这一行
maria_mmio_read & write
接下来我们需要看看核心的读写函数,由函数名,我们可以得到信息这两个函数的调用需要使用mmio的方式。为了让逆向代码可读性更好,我们需要将函数的第一个参数设置为对应的结构体类型(一般结构体的名称与设备名称相关,可以用关键词搜索)
右键指针类型,选择 Convert to struct *...
然后关键词搜索maria,设置对应类型。对write函数也进行同样的操作
我们就能得到相对直观的伪代码,下图是mmio_read
和mmio_write
cpu_physical_memory
我们注意到read和write使用cpu_physical_memory_rw()
来进行内存读写操作,需要注意的是,该函数用于物理内存的读写。因而我们需要对虚拟内存进行转换,这里我们先来关注下该函数的原型和使用方式
1 | void __cdecl cpu_physical_memory_rw(hwaddr addr, void *buf, hwaddr len, bool is_write) {}; |
漏洞分析及利用
这里的漏洞比较直观,主要是由于读写时的长度固定为0x2000,在设置偏移off
之后,便会导致越界读写。根据我个人调试的结果,成员变量src
具结构体开头的偏移0xa20,off
为偏移0xa28,buff
偏移为0xa30,结构体定义如下:
1 | typedef struct { |
可以看到越界读写便会读取结构体MemoryRegion
中的内容,里面有相当多的指针,具体我们会在后面介绍
因此基本利用思路如下:
- 利用越界读写,获得
qemu-system-x86_64
所在虚拟内存的堆地址指针和text段地址指针 - 覆盖
mmio->ops
,mmio->opaque
指针至合适的位置 - 覆盖
mmio_read
和mmio_write
以劫持控制流
Linux Huge Page
这里读写前需要考虑一个问题,内存读写的大小为0x2000,刚刚好是两个页面;同时又使用进程物理地址获取信息。所以我们还需要保证exp中的buf[0x2000]对应两个恰好连续的物理页面。这里解决方案使用的便是Linux Huge Page的方式,来扩大页表大小。
在申请buf前,使用如下方式开启Linux Huge Page
1 | system("sysctl vm.nr_hugepages=32"); |
具体介绍可以我之前的博客:Linux Huge Pages
MemoryRegion
MemoryRegion
结构体定义如下,我们可以找到ops
和opague
相对于该结构体起始地址的偏移为0x48和0x50
1 | struct MemoryRegion { |
需要知道的是opaque
即mmio函数的第一个参数,也就是指向PCI对应结构体的起始地址;ops
指向一个虚表,用于存放该PCI设备对应的一些函数,如下图
ops
指向的第一个函数为read,第二个函数为write;对于本题存在沙盒,我们只需要劫持虚表指针,覆盖maria_mmio_read
为mprotect
,maria_mmio_write
为shellcode即可。
函数参数rdi可以通过越界覆盖opaque指针完成,其他参数也可以直接控制,参考exp如下:
1 | /* |