首页
社区
课程
招聘
[原创]第10题wp
发表于: 2026-8-23 14:14 47

[原创]第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编辑 ,原因:
上传的附件:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回