首页
社区
课程
招聘
[原创]KCTF 2026 - 第十题:卯时·曦光初现 分析
发表于: 2026-8-23 16:07 742

[原创]KCTF 2026 - 第十题:卯时·曦光初现 分析

2026-8-23 16:07
742

目标文件:./pwn

程序入口 main(偏移 0x2d09)是一个 REPL:

因此主要攻击面在每行最多 255 字节的解释器语言。

main 是一个 REPL:

因此每一行都是一条独立语句,语句执行完后 AST 会被释放;但解释器变量和运行时对象会保留。

识别出的语句/表达式类型包括:

核心语法:

变量是固定 256 个 qword:

变量下标在执行时检查 <= 0xff。

程序初始化时分配一个 0x10000 字节的运行时对象堆,并从 /dev/urandom 读 8 字节作为 cookie:

执行 $n=[...] 时,会把对象序列化到这个堆里,并返回一个带标签的引用:

对象堆中每个 qword 都和 cookie 异或存储:

GetField / SetField 通过 sub_1EDA 在序列化对象中定位字段。定位逻辑会递归识别嵌套对象头:

也就是说:只要某个字段解码后的高 16 bit 是 0xBEEF,遍历器就把它当成嵌套对象头,并用低 48 bit 作为子对象大小来跳过它。

漏洞不是简单的 $obj[大下标] 越界;外层索引本身是有检查的。

问题在于:SetField 可以把任意 64-bit 值写入一个普通标量字段,只要求目标字段当前不是对象头;但它不禁止写入一个伪造对象头:

另一方面,虽然 Number 赋值会被截断到 48 bit,但 BinOp 的乘法、或、异或等运算是完整 64-bit。因此可以构造出高 16 bit 为 0xBEEF 的值:

把这样的值写入对象字段后,该字段就从“数字”变成了“嵌套对象头”。之后对该对象做一次外层合法的字段访问时,sub_1EDA 会信任这个伪造头,把后续字段跳过 attacker-controlled 长度,从而得到可控的前向越界读/写。

最小触发模型:

自定义堆是新分配的,未使用区域初始为 0。利用上面的越界读读到未使用堆字时:

所以 cookie 会作为普通变量进入解释器变量区。

一旦拿到 cookie,越界写任意原始值 X 就变成了:

因此 cookie 只能防止“不知道 cookie 时直接伪造堆数据”,不能阻止已经形成越界读写后的利用。

自定义堆结束后,相邻内存里有初始化阶段留下的指针。对题目 libc-2.27 环境,稳定可用:

所以可以在解释器内部计算:

这些值不需要打印出来;它们作为变量留在解释器里,继续参与后续运算即可。

每行执行完后,main 会调用:

如果能改写当前行 AST root 的 vptr,就能在执行结束后劫持控制流。

本题最终选择劫持一个 SetField 语句自己的 AST root:该语句在执行阶段完成最后一次越界写,返回后 main 立即调用它的虚析构。

题目 glibc-2.27 布局下,固定输入序列中最终 SetField root 的位置是:

补充:在调试机较新的系统 libc 环境下,这个偏移是 heap_base + 0x102a0;所以换 libc 时不仅要换符号偏移,也要重测 AST/堆布局偏移。

整体目标是:把“伪造对象头越界读写”升级为“改写当前 AST root 的 vptr”,再通过 setcontext pivot 到 ROP,执行 system("/bin/sh")。

先构造 0xBEEF000000000000:

之后每次需要一个 fake size K 时:

对于 $0=[0,0] 这种对象,字段 0 被伪造后,$0[1] 会落到对象外。

对于多字段对象 $0=[0,0,...,0],字段 0 的 fake size 固定为 K 时:

因此一个伪造对象就可以通过不同 j 连续写一段内存。

在题目 libc-2.27.so 中使用:

这些都在解释器内部用变量运算完成,不依赖程序打印。

最终要劫持的 SetField root 为:

在最终触发语句之前,用一个大多字段对象做连续写原语,把 final_root 后面预写成 fake vtable + ucontext + ROP。

关键布局是:

最后一条 SetField 语句执行时,把 final_root + 0x00 改成 final_root + 0x60。该语句返回后,main 调用:

也就是调用 setcontext(final_root)。

setcontext 从 fake ucontext 恢复寄存器:

随后栈上依次是:

执行效果:

使用 _exit 而不是 exit,是为了避免返回后进入 atexit/free 清理路径时触发堆检查 abort。

本题本质上是一个“序列化对象解释器”的内存破坏题。程序把对象以 cookie 异或后的形式存进自定义堆,并用 0xBEEF... 作为嵌套对象头、0x1337... 作为对象引用。漏洞点在于字段遍历逻辑会无条件信任字段中出现的 0xBEEF... 头,而 SetField 又允许把普通字段伪造成这种头;一旦伪造成功,递归 skip 就会按攻击者控制的长度跳过内存,形成稳定的前向越界读写。

利用上,先通过越界读未初始化零堆块拿到 cookie,使异或防护失效;再读取堆后固定位置的 _IO_file_jumps 和堆指针,在解释器内部算出 libc base 与 heap base;最后在当前 SetField AST root 附近预写 fake vtable、ucontext 和 ROP 链,并让该语句返回前改写自己的 vptr。main 调用 deleting destructor 时进入 setcontext,pivot 到 system("/bin/sh"),再用 _exit(0) 避开堆清理。整个利用不依赖程序回显变量值,所有地址都在解释器内部计算完成,最终实现远程 RCE 并读取 flag。

由 AI 完成。

$1=123                 # Number 赋值
$1=[1,2,[3,4]]         # Object 赋值,分配到自定义堆
$2=$1                  # 拷贝变量
$2=$1[0][1]            # GetField
$1[0][1]=$2            # SetField
$1+$2                  # BinOp,实际是 $1 = $1 + $2
$1-$2
$1*$2
$1/$2
$1%$2
$1^$2
$1|$2
$1&$2
uint64_t vars[256]; // PIE + 0x2062a0
heap_base = new char[0x10000]; // PIE + 0x206aa0
heap_cur  = heap_base;         // PIE + 0x206aa8
cookie    = read8("/dev/urandom"); // PIE + 0x206ab0
object_ref = 0x1337000000000000 | heap_addr;

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
点赞 ・4
打赏
分享
最新回复 (1)
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2 楼
6
2026-10-6 11:57
0
游客
登录 | 注册 方可回帖
返回