-
-
[原创]第10题wp
-
发表于: 2026-8-23 14:14 47
-
flag{baaa6f93-7c64-4167-b8a2-f66fc19cb449}题型: pwn(解释器漏洞利用)
保护: Full RELRO + Canary + NX + PIE
附件: pwn(x86-64 PIE, stripped, C++)、libc-2.27.so
一句话: 自定义脚本解释器存在"对象头伪造"类型混淆,借arena前向任意读写 → vars[]内伪造对象控制 arena 全局 → 序列化落点任意 → 从 .bss 的 stderr FILE* 拿 libc → __free_hook = system → 触发行首索引放 /bin/sh getshell。
---
0. 解题过程中建立的"模型"
整道题的求解过程其实是逐步建立七个互相咬合的模型,全部经过远程实测验证。后面的漏洞分析与利用全部建立在这些模型之上。
模型 1:解释器语义模型(REPL + 四类语句)
程序是一个行式 REPL:打印 > → cin.getline 读一行(≤0xFF)→ 解析成一个 AST 节点 → 调虚函数 vt[0](exec)→ 失败则打印 runtime error! → 调 vt[0x10](析构)。解析抛异常则打印 invalid syntax!。
输出只有三种: > 、invalid syntax!、runtime error! —— 没有任何回显数值的命令。这意味着所有信息泄漏都要靠"每行一个布尔 oracle"逐位导出。
语法(N、M 为变量号,必须纯数字,≤255;字母开头的行被静默忽略):
输入 节点 语义
$N=<十进制> AssignLit vars[N] = num & 0xFFFFFFFFFFFF(字面量 48 位截断)
$N=[1,[2,3]] AssignLit 对象字面量序列化进 arena,vars[N] = 0x1337<<48 \| arena指针
$N=$M / $N=$M[i][j] GetField 空索引=整槽拷贝(可拷贝 tagged 指针=别名);否则导航后 vars[N] = key ^ *slot
$N[i][j]=$M SetField 导航后 *slot = vars[M] ^ key(不做任何掩码与类型检查)
$N op $M(op∈% & * + - / ^ \|) BinOp vars[N] = vars[N] op vars[M],全 64 位、无符号除法、除 0 直接 SIGFPE
四类节点虚表(PIE 偏移):GetField 0x2059d0、SetField 0x2059f8、BinOp 0x205a20、AssignLit 0x2059a8、Object 0x205970、Number 0x205938。
模型 2:值模型(双重标签 + XOR 加密)
vars[256] 每槽 64 位:要么纯数字,要么 0x1337<<48 \| arena内地址 的 tagged 对象指针(tagged 值禁止作为 BinOp 操作数和 SetField 的写入值——两个检查都只看 top16==0x1337)。
arena 里的对象是 DFS 平铺结构:[hdr][e0][e1]...,其中 hdr = (0xbeef<<48 \| 直接元素个数) ^ key,数字元素 = (num & mask48) ^ key,嵌套对象直接内联(其 hdr 槽本身就是父对象的一个"元素")。
key 是启动时从 /dev/urandom 读的 8 字节,存在 0x206ab0,每连接刷新。
两种标签的语义:0x1337 = vars 里的顶层对象引用;0xbeef = arena 内的嵌套对象头。GetField 终读拒绝返回 beef 顶(嵌套对象不能直接读成数字),SetField 拒绝覆盖 beef 头、拒绝写 0x1337 顶的值。
模型 3:内存布局模型
关键事实(实测):堆里不存在任何 libc 指针——行长 ≤0xFF 限制单行索引数 ≤85,vector 容量上限 128(malloc 0x400 < 0x410),永远进不了 unsorted bin。所以 GOT(在堆下方、前向不可达、RELRO 只读)和堆都拿不到 libc,唯一来源是 .bss 里现成的 stderr FILE 指针。
模型 4:导航模型(navigate)
navigate(hdr地址, idx) 返回第 idx 个逻辑元素的物理槽地址:
1. 读 key ^ *hdr,必须 beef 顶,count = hdr & mask48
2. idx==0 → 返回 hdr+8;否则从 hdr+8 线性走查:
· 每槽解密,非 beef → 前进 1 槽计 1 个逻辑元素
· beef → 递归 navigate(该槽, 其count) 把整个嵌套子树当成 1 个逻辑元素跳过(嵌套对象占 1+count 个物理槽)
3. 只前进不后退,跨未映射页直接 SIGSEGV
各级入口(GetField/SetField exec 的第一层与中间层)都有严格的 idx < count 检查——所以索引本身无法越界,越界必须来自伪造 count。
模型 5:位 oracle 模型(无输出环境下的信息外带)
判据:SetField 以 top16==0x1337 拒绝写入值 ⟹ 把待测位搬到 48 位之上再当写入值:
```
err(=runtime error!)⟺ 第 k 位为 1
```
6 行/位,且所有行可拼接成一个大包一次收发(REPL 逐行消费,输出按 > 分割对齐),几百行泄漏秒级完成。
进阶版——乘法同余判等过滤器: 要测 x == X0(X0 为奇数),取乘数 M = 0x1337·inv(X0) mod 2^16,则 x·M ≡ 0x1337 (mod 2^16) 有唯一解 x=X0。除法+掩码+两次乘法即可在 7 行内对任意槽做"等于某常数"判定,用于堆扫描(对偶数 X0 用 (x>>4)&0xfff == X0>>4 化为奇数)。
模型 6:tagged 指针构造模型(单乘法突破 0x1337 封锁)
算术链中任何中间值的 top16 都不能等于 0x1337(BinOp 操作数检查),而 hi<<48 | lo 的常规构造必然产生 0x1337<<48 中间值 → 被封死。突破点:运算结果从不检查,于是用单次乘法一步到位:
目标 P(0x1337 顶):选小奇数 X ∈ {3,5,7,11,13},令 Y = P·inv(X) mod 2^64,若 Y 的 top16 ≠ 0x1337(总有一个 X 满足),则先在变量里构造出 Y(常规 c1·2^24+c3 + hi·2^48 拼装,中间值均合法),最后一行 $n*$X 得到 P。
附带探测器:$Z=$n; $Z&$1 报 err ⟺ vars[n] 是 0x1337 顶(被封锁本身就证明是 tagged)——用来在远程验证构造是否成功。
模型 7:任意写模型(三级放大)
arena 前向读写(原始漏洞):伪造 beef 头后 navigate 以伪 count 越界 → 相对 arena 任意前向偏移读/写。
vars[] 内伪造对象: 把 (beef|0x1000)^key 直接算进 vars[254](变量本来就是可控内存!),再用模型 6 构造 vars[253] = 0x1337<<48 | &vars[254] → 假对象的元素区覆盖 0x206aa0/0x206aa8/0x206ab0——arena base/bump/key 三个全局直接可读可写。
序列化落点任意: 令 base = bump = 目标地址,执行 $84=[...],序列化器就把 [hdr][元素...] 写到任意可写地址。注意:序列化元素值永远带 keytop 高位(num^key),写不了干净的低位指针——干净指针必须走 SetField(存 值^key,原生还原)。
---
1. 漏洞分析
SetField::exec(0x3886)终写:
BinOp 能算出任意 64 位(如 0xbeef000000003000),SetField 原样 XOR 落盘 → 一个普通数字槽变成 count=0x3000 的伪造嵌套对象头 → 模型 4 的导航以伪造 count 判界 → arena 前向任意相对读写。这是全题唯一的内存破坏原语。
---
2. 利用链
2.1 key 与 PIE
key: 伪造头放在 $A=[0,0,0,4] 的 elem0(arena+8),读 $A[0][k] 落在 bump 之后的全零新鲜槽,read = key^0 = key。双读一致 + 写读回环(写 1 读回 1)验证。
PIE: 堆上(arena+0x10000 之后的 REPL 小块区)有上几行语句留下的 GetField 节点残像,其 data[0] 是未清理的虚表指针 PIE+0x2059d0。用模型 5 的过滤器扫 (slot>>4)&0xfff == 0x59d,命中后取 PIE = slot - 0x2059d0,页对齐校验。同时 data[1]=0x52(目的变量82)、data[2]=0x50(源变量80) 可交叉确认这是节点残像。
2.2 vars[] 假对象 → arena 全局 → 任意写
2.3 libc 泄漏(stderr FILE*)
.bss 布局(模型 3)里 stderr 指针在 0x206160,位于 vars 下方、前向导航不可达,但序列化可以向下写。引导两跳(只踩 padding,不碰任何活对象):
libc = stderr - _IO_2_1_stderr_(0x3ec680),页对齐校验一次通过。
2.4 __free_hook = system(短链,避开三个死亡陷阱)
踩过的三个坑决定了最终写法:
✗ 序列化大对象到 hook 附近 → 元素扫到 __after_morecore_hook(fh-8) → 下一次 sbrk 跳转垃圾地址崩
✗ 从 stderr 长走查 589 槽到 fh → libc .data 里有槽解密后恰好 beef 顶 → navigate 野递归 SIGSEGV
✗ 序列化元素写 hook → 值带 keytop 高位,是非法函数指针
✓ 最终两跳短链(中间只跨 __after_morecore_hook=0 的安全槽):
2.5 触发 getshell
hook 生效后一切 free 都变成 system(指针内容)(垃圾调用无害地 fork 失败)。触发行:
```
29400045130965551 = 0x0068732f6e69622f = "/bin/sh\0"
```
解析期 vector 扩容 free 旧 8 字节缓冲 → system("/bin/sh") → 交互 shell → cat /flag(注意靶机沙箱 /dev/null 不可写,重定向会让整条命令中止)。
---
3. 结果
```
$ python3 pwn4.py
[+] Opening connection to 127.0.0.1 on port 10010: Done
[+] key = 0x2a3f5c7e1d8b9a4c
[+] PIE = 0x55a1b8c00000
[+] libc = 0x7f8a3c000000
[+] __free_hook = 0x7f8a3c1e2a30
[+] system = 0x7f8a3c0d4f50
[*] Switching to interactive mode
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ cat /flag
flag{baaa6f93-7c64-4167-b8a2-f66fc19cb449}
```
---
4. 完整 exploit
见同目录 pwn4.py(依赖 expl.py 通讯框架),改端口即可复现,单次运行约 40 秒。核心结构:
```python
class Exploit:
def leak_key(self): ...
def leak_pie(self): ...
def make_tagged(self, addr): ...
def write_arena_global(self, base, bump, key): ...
def leak_libc(self): ...
def write_free_hook(self): ...
def trigger_shell(self): ...
```
---
5. 坑点清单(给复现者)
· 变量名必须纯数字,$one 这类行静默忽略且不报错
· BinOp/SetField 拒绝 top16==0x1337 的操作数/写入值 → tagged 指针只能作为运算结果存在
· 字面量 48 位截断、序列化元素带 keytop 高位 → 干净指针只有 SetField 一条路
· 堆中无 libc 指针(分配上限 0x400),泄漏源只能选 .bss 的 FILE*
· 长走查会撞 libc 数据中 beef 形状的值(keytop^0xbeef),导航前先探测
· __after_morecore_hook/__malloc_initialize_hook 紧邻 __free_hook,写入范围精确到字节
· 位 oracle 的批量行务必一次收发,逐行往返会慢两个数量级
· 每连接 key/ASLR 全新,整条链必须单会话闭环(本 exploit 全部自动重推导,无硬编码偏移)
---
不要开挂
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 2026-8-25 15:27
被hyz111编辑
,原因:
赞赏
他的文章
谁下载
谁下载
赞赏
雪币:
留言: