第五空间云安全 qemu pwn writeup
0x00 分析逻辑
拿到题看到 launch.sh,启动了一个叫 ctf 的 pci 设备。把 qemu 拖到 ida 里面看一看,找 ctf 相关函数找到这么几个,明显就是 ctf 设备相关的几个函数了。
ctf_class_init
函数定义了设备号,厂家号,realize 函数和 uninit 函数。
pci_ctf_realize
函数做了 mmio 和 msi 中断的初始化。
题目的核心逻辑函数是 ctf_mmio_read
和 ctf_mmio_write
。
ctf_mmio_write
逻辑如下,没有完全标识:
1 | Write: |
分析一通下来,只凭直觉感觉 0x30 的这个 free 有问题,free 了之后只清了 size 和 inuse,留下了一个野指针,但是在 dma_rw 之前都有对 inuse 的 check。然后这个 v11 = (char *)opaque + 24 * opaque->note_idx;
,也很突兀,给人一种条件竞争的感觉,但是看了看 qemu 的启动命令,没有看到多核。
0x01 QTEST
无论如何先启动起来,这个 qemu 直接启动了一个 qtest 程序,qtest 可以读写物理内存以及做 io 操作,但是一次只能发送一条命令,所以条件竞争基本可以排除。
首先要解决的是交互问题,能交互就能动态调试,直觉告诉我只要能动态调起来,这个题的洞就知道是怎么回事(因为静态分析已经基本确定了漏洞的地方,要做的只是看怎么去触发这个漏洞)
然后问题就来了,mmio 映射到了哪块物理内存。。。。
我自己做了一个 linux kernel 镜像,在这个镜像里面 mmio 映射到了 0xfea00000。然后在 qtest 里面试一试。。。发现没用
又想起有 monitor,在 monitor 里面看了看,这 mmio 居然没映射。。。。
看起来题目就是让我们模拟操作系统映射 mmio 的操作,来做一遍 mmio 的映射。这就好办了,Google 大法好。搜了一圈发现只要在 PCI configuration space 里面写上相应的数据就可以了。也就是说要在 BAR0 写上我们 mmio 的地址就好了。按照文档里给的格式,0xcf8 是配置的地址,格式如下面函数所示;向 0xcfc 是这个地址里面的值。
1 | def config_addr(Bus, Dev, Fun, Reg): |
PCI configuration space 结构如下:
有一点要注意,这个 reg 参数是 0x4 对齐的,也就是一次得改 4 个字节, 32位。所以直接这样写入应该就能设置好映射。
1 | io_write32(0xcf8, config_addr(0, 2, 0, 0x10)) |
然而。。。。。。。。。。。。。。。。。。并不行
没什么思路就继续各种 Google。看到了 flynn 的博客 https://blog.csdn.net/weixin_43780260/article/details/104410063。
跟着调试我自己放进去的内核,想看看内核是在什么时候把 ctf 设备的 mmio 地址映射到物理地址 0xfea00000 的。对 ctf 设备的 mmio 地址下个内存断点,看看什么函数改过它。的确跟到了 pci_update_mapping
但是还不是设置成 0xfea00000,和博客中描述的一样。再往后跟,看到修改为 0xfea00000 的调用栈,一层一层地回溯,发现是往某个 io 地址写入了 0x103 之后才调用的 pci_update_mapping
把 mmio 映射到 0xfea00000。
然后发现还是在这篇博客中。。。
所以继续向 command 字段中写入 0x103,果然 mmio 可以正常工作了。然而这个时候比赛也结束了。。。
后来在和 TD 的师傅水群的时候,知道了这个 qtest 是强网杯的原题。当时这是个二连 pwn,我们队只有 afang 把第一问做了,第二问没人看,之后也没复现,血亏。
回到正题,mmio的事情解决了,不过测试发现 dma 没办法正常使用。发现断在调用链 address_space_rw -> address_space_read_full -> flatview_read -> flatview_read_continue
里面判断条件没有过。
这就很麻烦了,分析了一通这个条件,也不知道为什么没过。所以这题就搁置在这了。过了几天之后想先看看强网杯 qtest 的 wp,于是就找到了这个。。。https://matshao.com/2021/06/15/QWB2021-Quals-EzQtest/
直呼我是散兵。把 command 字段从 0x103 改成 0x107,dma 也能正常工作了。
0x02 漏洞
盯了半天那个可能的漏洞点,也没发现有啥利用手段,只是越看越像条件竞争,google 了很长时间,qtest 文档读穿了也没看到一行多命令的支持。然后就去读qemu 的 dma 源码,就在 flatview_read_continue
里,发现这 dma 可以读写 mmio 区域。。。。这就能解释后面这个类似条件竞争的 double fetch 了。
0x03 利用思路
这样思路也就呼之欲出了,在 free 之前可以通过 dma 语句把 mmio_base + 0x40 的 note_idx 改掉,这样就不会把刚刚 free 掉的空间的 size 和 inuse
置 0 了,就产生了一个标准的 UAF,剩下的利用傻子都会了。经过一波简单的堆风水之后,成功打到 free hook 并触发 system(“/bin/sh”)。
0x04 EXP
1 | def writeq(addr, val): |