首页
社区
课程
招聘
KCTF 第十题:卯时·曦光初现 - PWN Writeup
发表于: 1天前 386

KCTF 第十题:卯时·曦光初现 - PWN Writeup

1天前
386

难度:中上 | 类型:VM/解释器 pwn | 环境:Ubuntu 18.04 + glibc-2.27
最终利用:23 行 VM 语句、零地址外泄、ASLR 免疫、单连接稳定 getshell

程序是一个交互式 REPL 解释器,提示符 > ,每行 getline(0x100) 读一条语句。C++ 编写,namespace interpreter,RTTI 泄露出类名:Literal / Number / Object / Ast / AssignLit / GetField / SetField / BinOp

唯一输出只有三种:提示符 > invalid syntax!(解析失败)、runtime error!(求值失败),以及进程崩溃(连接断开)。没有任何数值回显通道——这是本题的核心难点。

注意 第二个虚调用槽是 +0x10(不是 +8),且 rdi = 根节点自身。这是最终利用的跳板。

裸表达式、$N[i]=字面量、多语句拼接均 invalid syntax。

64 位值,高 16 位是 tag:

arena 内每个 qword 都以 ^key 存储:对象头 = (0xbeef<<48|count)^key,数字字段 = value^key。GetField 读 [addr]^key,SetField 写 value^key——读写自洽,XOR 对 arena 内操作完全透明。

BinOp::eval @0x30de——漏洞所在:

字段游走 @0x1eda:从对象头地址线性扫描,每字段 8 字节;若字段值(^key 后)带 0xbeef tag 视为嵌套对象,递归跳过 count 个字段。头检查 count >= index

GetField::eval @0x36e6:源寄存器必须 0x1337 tag;沿路径游走;最终读出的值若带 0xbeef tag 则拒绝(防读原始头)。

SetField::eval @0x3886:写入值(vars[src])不得带 0x1337 tag;目标对象(vars[dst])必须带;每层头检查;最终写入位置当前值(^key 后)带 0xbeef tag 则拒绝(防覆写头)。

乘法可以把任意值推入高 16 位。BinOp 只拦输入 0x1337,不拦输出,于是:

此后 $0[0][N](GetField)/$0[0][N]=$V(SetField)把 arena+8 当对象头游走:第 N 个字段在 arena+16+8N。count=8300 ⇒ 窗口 = arena+16 … arena+66416,覆盖整个 64KB arena 外加 arena 之后约 2KB 的堆——那里有每行解析分配的 AST 节点、locale 结构、以及稳定的运行时指针。

key、libc、heap 全是运行时随机值且无法打印。但根本不需要打印:

因此整条链天然 ASLR 免疫,也无需任何 oracle 逐位外泄。

在 docker ubuntu:18.04(系统 libc 与题目 buildid 完全一致)中用 gdb 逐槽 dump,确认 arena 之后的堆:

两个派生常数(仅依赖二进制+libc 版本,与运行环境无关):

注意:索引全部相对 arena,ASLR 只平移基址,不影响索引;但行序列会改变 tcache 状态 ⇒ payload 必须逐行原样发送(实测 23 行版与 65 行版各自稳定)。

最后一行语句的求值时序是关键:

写入发生在虚调用之前,而且写的就是"当前这行"的根节点 vtable 槽(索引 8280)——自引用闭环:

没装 one_gadget 工具,直接 objdump 扫 lea rdi,[rip+X](X 落在 /bin/sh 字符串 0x1b3d88)且 8 条指令内 call execve,得 4 个候选;逐一实测约束:

argv 是垃圾字节但 execve 只要求可读;sh 起来后继续从 socket stdin 读命令——直接就是交互 shell

顺带一提为何不用 system:虚调用点 rdi=根节点,同一 qword 既要当 vtable 指针被解引用、又要当命令字符串——"/bin/sh\0" = 0x0068732f6e69622f 是不可映射地址,物理矛盾。故必须用不依赖 rdi 的 execve gadget。

题目本质是三层套娃:

三者缺一不可,最终 23 行语句稳定起 shell。

工具链:lief + capstone(静态)、docker ubuntu:18.04 + gdb(动态)、纯 socket(打点)。

语句 语义
$N=<十进制> vars[N] = 数字(仅低 48 位)
$N=[1,2,3] 建对象(inline 序列化进 arena),vars[N]=句柄
$N=$M 寄存器拷贝
$N=$M[i][j]… GetField:沿路径读字段
$N[i][j]…=$M SetField:写字段(RHS 必须是寄存器,字面量 → invalid syntax)
$N<op>$M BinOp:vars[N] = vars[N] op vars[M],op ∈ + - * / % ^ | &
top16 含义 低 48 位
0x1337 对象引用(句柄) 绝对指针
0xbeef arena 内对象头 字段数 count
其他 数字 数值(仅低 48 有效)
OOB 索引 地址(arena+) 内容 稳定性
8193 +0x10018 heap+0x10(tcache 结构指针,启动时写入) ✅ 恒定
8205 +0x10078 libstdcxx+0x2d680(locale 结构里的 facet vtable) ✅ 恒定
8280 +0x102D0 当前行 SetField AST 节点的 vtable 槽(tcache LIFO 复用同一 0x30 chunk) ✅ 对固定 payload 恒定
候选 入口约束 实测
0x4f2c5/0x4f322 do_system 内部,fork 失败路径,寄存器状态不满足 ✗ 崩在 do_system+1094
0x10a308 [rsp+0x70]==NULL ✗ 栈非零
0x4f302 rsi=rsp+0x40(栈上内容当 argv,可读即可),rdx=*environ 成功
pwn           ELF64 x86-64 PIE, stripped, C++, Full RELRO(BIND_NOW), NX, canary
libc-2.27.so  Ubuntu 18.04 (buildid f7307432a8b162377e77a182b6cc2e53d771ec4b)
remote        nc 123.57.66.184 10053
0x2e38  打印 "> "  (ostream_insert)
0x2e84  getline(cin, buf, '\n')
0x2ea2  用 buf 构造 std::string(>15 字符则堆分配)
0x2ed5  call 0x3d38          ; 递归下降解析 → 返回 AST 根节点 rax
0x2df9  rbx=rax; 释放行字符串; 若 rbx==0"invalid syntax!"
0x2e25  call [rbx+0x00]      ; ★ 虚调用 vtable 槽 0 = eval(rbx, vars)
0x2e27  test al,al → 0"runtime error!"
0x2e35  call [rbx+0x10]      ; ★ 虚调用 vtable 槽 +0x10 = deleting dtor, rdi=根节点

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

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回