虚拟化逃逸的攻击面及交互方式
最近在CTF中QEMU逃逸题目出现的频率也越来越高了,在这里记录一下常见的攻击面和进行交互的方式。对于虚拟化逃逸来说,其实就是打hypervisor这个用户态的程序,所以在明白交互的原理之后流程就和一般的pwn差不多了。
Device
这应该是最常见的一种形式了,在qemu中新实现一种设备并引入漏洞,guest通过与设备进行交互进行漏洞利用。
在host上使用下面的命令来查看qemu支持的设备
1 | ./qemu-system-x86_64 -device help |
也可以boot到guest之后使用lspci -nvv
来查看pci设备的具体信息。
Linux kernel与设备通信有PIO, MMIO, DMA等形式,通常是kernel会实现一套driver在内核态与device交互,再开放API供用户态程序使用。
但其实在用户态也是可以直接访问到设备的,这就意味着我们可以直接编写用户态的程序来进行交互了。
Port I/O
PIO可以算是最原始的一种模式了,CPU执行in
/out
指令来向I/O端口发送数据。cat /proc/ioports
能够看到目前port的一些信息和它们对应的设备。
in
/out
指令是特权指令,一般只有在ring 0时才能执行,linux内核提供了 inb
/ outb
/ inw
/ outw
/ inl
/ outl
,用于向port发送不同长度的数据。
不过在linux中可以通过 iopl
ioperm
这两个系统调用对port的权能进行设置。
1 |
|
ioperm
可以将from
之后的num
个port打开供用户态程序使用,当在用户态执行in
/out
对这些port进行读写时不会触发fault.
这个调用需要进程是root权限或者具有CAP_SYS_RAWIO
权能。
另外,还可以通过打开/dev/port
这个文件,再lseek
到对应port的offset进行读写,同样可以实现PIO.
如果没有/dev/port
文件,可以使用 mknod -m 660 /dev/port c 1 4
来创建。
CTF中的例子可以参见SECCON 2018的q-escape
MMIO (Memory-mapping I/O)
另一种进行交互的方式是使用MMIO,CPU将一部分设备的寄存器映射到一段特殊的物理内存地址,当进行访存时就相当于对这些寄存器进行操作。
cat /proc/iomem
能够看到与I/O相关的一些内存地址信息。
通过kernel提供的sysfs,我们可以直接映射出设备对应的内存,具体方法是打开类似 /sys/devices/pci0000:00/0000:00:04.0/resource0
的文件,并用mmap
将其映射到进程的地址空间,就可以对其进行读写了。这里的设备号0000:00:04.0
是需要事先在/proc/iomem
中看好的。当映射完成后,就可以对这块内存进行读写操作了,内存读写会触发到qemu内设备的mmio处理函数(一般会叫xxxx_mmio_read
/xxxx_mmio_write
),传入的参数是写入的地址偏移和具体的值。
在qemu这边,需要对这块内存区域用memory_region_init_io
进行注册,传入MemoryRegionOps
结构,提供相应的读写处理函数。
另外,还可以通过打开/dev/mem
这个文件直接操作物理内存,mmap
设备对应的地址进行读写,也是相同的效果。
如果没有/dev/mem
文件,可以使用 mknod -m 660 /dev/mem c 1 1
来创建。
这里有一个我经常使用的交互模板
1 |
|
DMA
8会:)
Hypercall
hypercall是guest kernel向hypervisor发出的一种调用形式,广义上说像vmtools的RPCI机制使用特定I/O端口实现的也可以称之为hypercall. 还有一种方式是通过一些特权指令,例如 vmmcall
.
即使不在kvm模式(KVM的hypercall实现可以参考我之前的一篇abyss writeup),QEMU也仍然可以通过TCG实现hypercall,具体来说就是在进行binary translation时,为vmmcall
定制helper函数,完成特定的功能就可以了。
此次XCTF Final中的QTCG就是这样实现的,但这样一来我们需要直接执行vmmcall
指令,这如果在用户态执行会产生fault,所以我们就在内核态用kernel module来完成利用。
编写一个可以进行简单交互的kernel module pwn.c
1 |
|
再编写一个对应的makefile,因为kernel module与linux的内核版本是紧密相关的,根据kernel的文档,在有kernel源码的时候,可以直接使用Kbuild的Makefile来build. 当然kernel源码也需要事先build一些东西,例如 make modules_prepare
,具体可以参看文档。
1 | obj-m := pwn.o |
通过指定 obj-m
变量告诉Kbuild我们build的文件名pwn
,在执行make
之后,会生成对应的pwn.ko
.
对于当前运行的内核进行编译,makefile可以这样写(需要安装kernel headers包).
1 | obj-m := pwn.o |
总结
完成hypervisor的漏洞利用对只做过用户态pwn的同学来说是一个比较大的考验,不仅要掌握hypervisor这边的一些实现原理,更需要对guest内部的kernel工作机制有所了解,而这其实也正是计算机系统最底层的知识。
如果在这方面基础薄弱(计原,操统没好好学)的话,还是应该补补课,算是系统安全的基本素养之一。
将虚拟化逃逸和kernel pwn放在一起来学习,也是一个不错的选择。
Reference
- 《Linux设备驱动程序》
- sysfs-pci.txt
- Building External Modules
- QEMU Memory