说在前面
本文的草稿是边打边学边写出来的,文章思路会与一个“刚打完用户态 pwn 题就去打 QEMU Escape ”的人的思路相似,在分析结束以后我又在部分比较模糊的地方加入了一些补充,因此阅读起来可能会相对轻松。(当然也不排除这是我自以为是)
[1] 题目分析流程
[1-1] 启动文件分析
读 Dockerfile
,了解到它在搭起环境以后启动了start.sh
,
再读 start.sh
,了解到它启动了 xinetd
程序
再读 xinetd
,这个程序的主要作用是监听指定 port,并根据预先定义好的配置来启动相应服务。可以看到 server_args
处启动了 run.sh
再读 run.sh
,发现它用 QEMU 起了一个程序,通过 -device vn
我们可以知道 vn
是作为 QEMU 中的一个 pci设备
存在的。
通过 IDA 查找字符串 vn_
可以找到 vn_instance_init
,跟进调用 字符串vn_instance_init
的 函数vn_instance_init
,再按 x 查看 函数vn_instance_init
的引用,可以看到下面还有一个 vn_class_init
,反汇编后看到
__int64 __fastcall vn_class_init(__int64 a1)
{
__int64 result; // rax
result = PCI_DEVICE_CLASS_23(a1);
*(_QWORD *)(result + 176) = pci_vn_realize;
*(_QWORD *)(result + 184) = 0LL;
*(_WORD *)(result + 208) = 0x1234; // 厂商ID (Vendor ID)
*(_WORD *)(result + 210) = 0x2024; // 设备ID (Device ID)
*(_BYTE *)(result + 212) = 0x10;
*(_WORD *)(result + 214) = 0xFF;
return result;
}
通过厂商ID和设备ID,我们可以判断下列 pci 设备中 00:04.0 Class 00ff: 1234:2024
就是我们要找的 vn
/sys/devices/pci0000:00/0000:00:04.0 # lspci
lspci
00:01.0 Class 0601: 8086:7000
00:04.0 Class 00ff: 1234:2024
00:00.0 Class 0600: 8086:1237
00:01.3 Class 0680: 8086:7113
00:03.0 Class 0200: 8086:100e
00:01.1 Class 0101: 8086:7010
00:02.0 Class 0300: 1234:1111
进而去/sys/devices/pci0000:00/0000:00:04.0
目录查看该设备 mmio
与 pmio
的注册情况
/sys/devices/pci0000:00/0000:00:04.0 # ls -al
...
...
-r--r--r-- 1 0 0 4096 Feb 18 12:18 resource
-rw------- 1 0 0 4096 Feb 18 12:18 resource0
...
...
有了 resource0 这个文件,我们就可以在exp里 mmap
做虚拟地址映射。
并且我们可以看到 vn
这个设备只注册了 mmio
,那就考虑用 mmio攻击(点击这里了解 mmio 运行原理)
[1-2] 静态分析
如果我写的不够清楚,读者可以参考 blizzardCTF 里的 strng这一实现,读完这段代码会对 pci 设备的了解提升一个台阶。
我们先补充一些概念:
QEMU 提供了一套完整的模拟硬件给 QEMU 上的 kernel 来使用,而 -device
参数为 kernel 提供了模拟的 pci 设备。
如果 kernel 实现了类似 linux 的 rootfs,我们就可以通过 lspci
来查看相关 pci,并在/sys/devices/...找到 pci 设备启动时 kernel 分配给 pci 的资源,也就是 resource0 等,这也是前文提到过的。
resource0 可以看作是一大片开关,当我们修改 resource0 中的内容时,可以看做对应开关被启动,pci设备也随着开关的启动而变化,具体表现为“控制寄存器、状态寄存器以及设备内部的内存区域 随着 resource0 的变化而变化”
所以我们可以 open resource0 这个文件,用 mmap 映射它,从而使我们能够在C代码中对 resource0 这片内存进行修改
可是由于 QEMU 也只不过是一个程序,虚拟的 pci 设备意味着,一定有一片内存存储着 pci 相关的数据
关于 pci 存储数据的这一部分好像就涉及 QOM 了,还没太搞懂,总之跟pci_xx_realize, xx_class_init, xx_instance_init 等函数有关
假设我们的调用链是这样的:
docker -> QEMU -> exp
则 docker 会让 QEMU 误以为自己占据全部内存空间,QEMU 会让 exp 认为自己占据全部内存空间
而 QEMU 的 pci 设备的 MemoryRegion 就存储在 QEMU 的堆区上,我们在程序 exp 中读写 resource0,就相当于操控 vn_mmio_read 和 vn_mmio_write 去读写 QEMU 的堆区,如果我们正好修改到 MemoryRegion 的 xx_mmio_ops 指针,就可以劫持控制流。
那么,接下来我们要做的事情就是去读一下 vn_mmio_read 和 vn_mmio_write 的反汇编,了解怎样读写堆区内容。
由于对 QEMU 不是很熟悉,我只能瞎命名,vn_mmio_write 的大体逻辑是