-
-
KCTF2026: 第十题:卯时·曦光初现
-
发表于: 1天前 348
-
Flag: flag{ac2aec65-aeb5-4760-9d90-228064e4db4b}
程序是一个小型 REPL。RTTI 残留了 interpreter 命名空间下的类名:值类Number / Object,以及 AST 节点类 AssignLit / GetField / SetField /BinOp。每行一条语句:
空行会重新出提示符;非法语句打印 invalid syntax!;执行失败打印runtime error!。程序没有任何“打印数值”的功能 —— 唯一反馈就是成功/报错
(这一点很关键:它逼着我们把整条利用都在 env 内部算完)。
所有值以 NaN-boxing 方式存放在一个 256 槽的全局 env[] 里:
数组内容被扁平化存进一块固定的 0x10000 字节 bump 缓冲区 arena
(初始化时 operator new[](0x10000))。每个存进去的字都会和一个启动时从/dev/urandom 读取的 8 字节 key 做 XOR 混淆(放在 .bss 固定偏移)。一个数组
序列化后的布局:
env[](256×8)紧挨在 arena 控制块之前;arena 本身是普通堆缓冲区,其后就是进程
的其余堆区。
BinOp::execute 计算 env[dst] op env[src] 后把结果写回,既不截断到 48 位、
也不重新检查 tag(只拒绝 0x1337 的操作数)。于是算术运算可以合成出任意
64 位字 —— 关键是构造出一个伪数组头 0xbeef<<48 | BIGLEN。
用 SetField 把这个字写到某个已有元素上,该元素就变成了一个长度任意的伪嵌套
数组。再对它取下标(GetField/SetField)就会读/写arena_elem_base + 8 + k*8(k < BIGLEN),即对 arena 及其后堆区的越界读写。
(下标遍历会逐字扫过中间内存,所以可达范围是“连续映射的堆” —— 已经足够。)
其余机制(256 槽上限、0xbeef 头校验、每层下标边界、XOR key)都是缓解措施;
唯一的裂缝就是 BinOp 未掩码的结果。
每个命令 AST 对象都是带 vtable 的堆对象;main() 会先调用obj->vtable[0](env)(execute),在 execute 返回之后再调用obj->vtable[2]()(析构,即 call [rax+0x10],此时 rdi=obj)。而一个SetField 对象会稳定地复用在 arena+0x10430(0x30 大小的 tcache 复用;该相对
偏移与 ASLR 无关)。
全程无需把地址外带给自己:one_gadget 地址与 arena 指针都在 env[] 里算出,越界写
只负责把它们放到位。
稳定性≈100%(配合小重试循环)—— 唯一的失败来自约 0.2% 的概率:随机 key 恰好让某个
被遍历到的堆字解混淆后高 16 位等于 0xbeef(被误判成嵌套头)。本地 8/8,远程一
把即中。
本地运行直接用加载器(可移植,无需 patchelf):./ld-2.27.so --library-path . ./pwn。其中 ld-2.27.so / libstdc++.so.6 /libgcc_s.so.1 / libm.so.6 取自官方 Ubuntu-18.04 软件包
(libc6 2.27-3ubuntu1.6、gcc-8 …~18.04);libc 的 build-id 与题目所给libc-2.27.so 完全一致。
| 语法 | 节点 | 效果 |
|---|---|---|
$N=<num> |
AssignLit | env[N] = num & 0xffffffffffff |
$N=[a,b,…] |
AssignLit | env[N] = 数组句柄(元素可嵌套) |
$N=$M[i][j]… |
GetField | env[N] = env[M] 取下标(读) |
$N[i][j]…=$M |
SetField | 把 env[M] 写入 env[N] 的下标处(写) |
$N op $M |
BinOp | env[N] = env[N] op env[M],`op ∈ + - * / % ^ |
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。