【学习记录】Week10(三):Tcache 溢出与扩展利用——单链表劫持与高版本绕过
写在前面:在上一篇中,我们重温了经典的 Unlink 利用,学习了如何通过堆溢出覆盖双向链表的
fd/bk指针实现任意地址写。随着 glibc 2.26 引入 Tcache,堆的管理机制发生了翻天覆地的变化。Tcache 像一个“快速通道”,优先处理小内存的分配和释放,且几乎没有安全校验。今天,我们将探讨在 Tcache 时代,堆溢出如何直接劫持这条单链表,以及在高版本 glibc 中如何结合新特性进行扩展利用。
📑 目录
- Tcache 机制回顾与溢出优势
- 基础利用:Tcache Poisoning(溢出版)
- 高版本绕过:Safe-Linking 机制与堆地址泄露
- 扩展利用:Tcache Stashing Unlink Attack
- 实战演练:构造高版本 Tcache 溢出链
- 总结与下篇预告
1. Tcache 机制回顾与溢出优势
1.1 Tcache 结构简析
Tcache(Thread Local Caching)为每个线程维护了一个本地的空闲链表数组。在 64 位系统中,它默认管理 0x20 到 0x410 大小的 chunk。
核心结构如下:
typedef struct tcache_entry { struct tcache_entry *next; struct tcache_perthread_struct *key; // glibc 2.29+ 引入,用于检测 double free } tcache_entry; typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; // 每个_bin的chunk计数 tcache_entry *entries[TCACHE_MAX_BINS]; // 单链表头指针数组 } tcache_perthread_struct;Tcache 链表是一个单链表,遵循后进先出(LIFO)原则。释放时 chunk 插入链表头部,分配时从链表头部取出。
1.2 为什么 Tcache 下的溢出更简单?
在传统的 Unlink 利用中,我们需要构造复杂的双向链表并绕过FD->bk == P的检查。而在 Tcache 机制下:
- 无双向链表检查:Tcache 是单链表,只需要修改
next指针,没有任何指针完整性校验(在 glibc 2.32 之前)。 - 优先级最高:只要 Tcache 对应的 bin 中有 chunk,
malloc会直接从 Tcache 取,不会经过 fastbin 或 unsorted bin 的复杂逻辑。 - 直接劫持:只要我们能通过溢出覆盖 Tcache 链表头部 chunk 的
next指针,下一次malloc就会返回我们伪造的任意地址。
2. 基础利用:Tcache Poisoning(溢出版)
Tcache Poisoning(Tcache 投毒)是指通过漏洞修改 Tcache 链表中 chunk 的next指针,使其指向任意地址,从而在后续分配时获取该地址的控制权。如果是通过堆溢出实现的,就称为 Tcache 溢出利用。
2.1 利用原理
假设当前 Tcache 链表(大小 0x90)状态为:head -> chunk_A -> NULL。
如果我们通过堆溢出(比如溢出 chunk_A 前面的 chunk),覆盖了 chunk_A 的next指针,将其改为target_addr。
链表状态变为:head -> chunk_A -> target_addr。
此时:
- 第一次
malloc(0x80):返回chunk_A,链表头变为target_addr。 - 第二次
malloc(0x80):返回target_addr!
2.2 利用流程图
堆布局: [可控 Chunk] [Tcache Chunk A]
利用溢出覆盖 Chunk A 的 next 指针
next = target_addr
链表状态: head -> A -> target_addr
第一次 malloc(size)
返回 Chunk A
链表头变为 target_addr
第二次 malloc(size)
返回 target_addr
实现任意地址写
3. 高版本绕过:Safe-Linking 机制与堆地址泄露
随着 glibc 版本的升级,Tcache 的安全性也在增强。在 glibc 2.32 中,引入了Safe-Linking机制,对 Tcache(和 fastbin)的next指针进行了加密。
3.1 Safe-Linking 原理
该机制将 chunk 的next指针与其自身地址的高 12 位(实际上是地址右移 12 位)进行异或运算后存储。
// 存储时 #define PROTECT_PTR(pos, ptr) ((void *)((((size_t)pos) >> 12) ^ ((size_t)ptr))) // 读取时 #define REVEAL_PTR(ptr) PROTECT_PTR(&ptr, ptr)目的:为了防止攻击者在不知道堆地址的情况下直接伪造next指针(例如直接写入一个 GOT 表地址)。现在,如果你要伪造next指向target_addr,你必须知道当前 chunk 的真实地址,计算(chunk_addr >> 12) ^ target_addr并写入。
3.2 绕过方法:泄露堆地址
要绕过 Safe-Linking,核心是获取堆基址或某个堆块地址。
通常的方法是:
- 利用 UAF 或未初始化漏洞,泄露一个堆块中的
next指针(加密后的值)。 - 由于
next是(chunk_addr >> 12) ^ next_chunk_addr,如果该 chunk 是链表末尾(next实际为 0),则加密后的值就是(chunk_addr >> 12)。 - 通过这个值恢复出堆地址的高位(右移 12 位的值)。
- 在溢出构造 payload 时,使用泄露的堆地址计算出正确的异或密文:
encrypted_next = (leaked_heap_base << 12) ^ target_addr。
4. 扩展利用:Tcache Stashing Unlink Attack
当程序使用calloc分配内存时(calloc不从 Tcache 获取 chunk),或者当 Tcache 满了,chunk 会进入 smallbin。当从 smallbin 中取出 chunk 时,如果对应大小的 Tcache bin 未满,glibc 会将 smallbin 中剩余的 chunk 放入 Tcache 中。这个“stash”(暂存)过程存在一个经典的利用链:Tcache Stashing Unlink Attack。
4.1 触发条件
- 存在堆溢出,能修改 smallbin 中最后一个 chunk 的
bk指针。 - 对应大小的 Tcache bin 未满(通常需要先填满再释放几个,或控制 counts)。
- 使用
calloc触发从 smallbin 的分配。
4.2 利用原理
当从 smallbin 取出最后一个 chunk(设为 victim)时,如果 Tcache 还有空位,glibc 会遍历 smallbin 剩余的 chunk 并放入 Tcache。遍历的依据是 victim 的bk指针。
// glibc 源码片段 bck = victim->bk; bin->bk = bck; bck->fd = bin; // 关键漏洞点:向 bck->fd 写入 bin 的地址 // 随后 victim 被放入 Tcache tcache_put(victim);如果我们通过溢出,将 victim 的bk修改为target_addr - 0x10(在 64 位下,fd字段在 chunk 头部偏移 0x10 处)。
那么执行bck->fd = bin时,就会向target_addr写入一个 main_arena 的地址(即 libc 地址)。
同时,由于后续的 stash 逻辑,target_addr也会被作为一个 chunk 放入 Tcache 链表。这意味着我们不仅实现了向任意地址写入 libc 地址,还实现了将任意地址加入 Tcache 链表,后续可以通过malloc获取该地址。
4.3 应用场景
- 修改
global_max_fast:将其改写为一个很大的值(libc 地址通常很大),使得很大的 chunk 也能被当作 fastbin 处理,从而利用 fastbin attack。 - 伪造
_IO_list_all:为 FSOP(File Stream Oriented Programming)攻击做准备。
5. 实战演练:构造高版本 Tcache 溢出链
让我们结合前面的知识,构造一个在 glibc 2.32 环境下的 Tcache 溢出利用伪代码。
5.1 场景设定
- glibc 版本:2.32(启用 Safe-Linking)
- 漏洞:
edit函数存在堆溢出,可以修改下一个 chunk 的内容。 - 目标:通过 Tcache Poisoning 分配到
__free_hook,覆盖为system。
5.2 利用步骤与 Payload 构造
步骤 1:泄露堆地址
# 1. 分配并释放一个 chunk 进入 Tcache add(0, 0x28) # chunk A free(0) # chunk A 进入 Tcache[0x30] # 2. 利用 UAF 或 show 功能读取 chunk A 的 next 指针 # 此时 A 是链表尾,next 实际为 0,加密后存储为 (A_addr >> 12) ^ 0 leaked_ptr = show(0) heap_key = u64(leaked_ptr.ljust(8, b'\x00')) # 这就是 A_addr >> 12步骤 2:布置堆块
# 分配 chunk B, C, D # B 用于溢出,C 是目标 Tcache chunk,D 防止合并 add(1, 0x28) # chunk B (与 A 重合或新分配,看具体题目逻辑) add(2, 0x28) # chunk C (将被 free 进 Tcache) add(3, 0x28) # chunk D # 释放 C,此时 Tcache[0x30]: head -> C -> (如果A还在则连A) free(2)步骤 3:构造溢出 Payload
假设我们要让malloc返回__free_hook的地址。
我们需要覆盖 chunk C 的next指针。由于 Safe-Linking,我们要写入的值是:encrypted_next = (heap_key) ^ __free_hook_addr(注意:heap_key实际上是 C 的地址右移 12 位,如果 C 的地址和 A 不同,需要计算偏移。假设我们泄露的就是 C 的地址的 key)。
free_hook_addr = libc_base + libc.sym['__free_hook'] # 假设泄露的 heap_key 就是 chunk C 对应的 key (C_addr >> 12) # 如果不是,需要根据堆布局推算 C_addr c_addr_key = heap_key # 简化场景 encrypted_next = c_addr_key ^ free_hook_addr # 构造溢出 payload payload = b'A' * 0x28 # 填满 chunk B payload += p64(0) + p64(0x31) # 伪造 chunk C 的 header (prev_size + size) payload += p64(encrypted_next) # 覆盖 chunk C 的 next 指针 edit(1, payload) # 触发溢出步骤 4:触发分配获取 Hook
# 第一次 malloc 返回 chunk C add(4, 0x28) # 第二次 malloc 返回 __free_hook add(5, 0x28) # 此时 chunk 5 的指针指向 __free_hook # 向 chunk 5 写入 system 地址 edit(5, p64(libc_base + libc.sym['system'])) # 释放一个包含 "/bin/sh" 的 chunk,触发 system("/bin/sh") add(6, 0x28, b'/bin/sh\x00') free(6)6. 总结与下篇预告
6.1 核心知识点总结
- Tcache 简化了利用:单链表结构且无校验,使得堆溢出覆盖
next指针即可实现任意地址分配,无需复杂的 Unlink 绕过。 - Safe-Linking 是新屏障:glibc 2.32 引入的指针加密机制,要求必须泄露堆地址(右移 12 位的值)才能正确伪造
next指针。 - Tcache Stashing Unlink Attack:利用
calloc不走 Tcache 的特性,结合 smallbin 到 Tcache 的暂存机制,实现向任意地址写入 libc 地址并加入 Tcache 链表的高级利用。 - 溢出的核心作用:在 Tcache 体系下,溢出主要用于篡改链表指针(
next)或篡改 smallbin 指针(bk),从而劫持内存分配流。
6.2 下篇预告
下一篇,我们将迎来 Week10 的收官之战——综合练习:off-by-one + tcache 组合题。我们将结合第一篇的 Off-by-one 漏洞和本篇的 Tcache 利用,模拟一道完整的 CTF PWN 题目,从漏洞分析、堆布局设计到最终 EXP 构造,手把手实战演练!
最终结论:Tcache 机制既是福音也是诅咒。它极大地简化了小内存堆漏洞的利用路径,但也催生了如 Safe-Linking 等新的防护机制。掌握 Tcache 溢出利用,是现代 CTF PWN 选手的必备技能,也是理解高版本 glibc 堆利用的关键。
参考文献:
- CTF Wiki - Heap Exploitation: Tcache
- glibc 2.32 Safe-Linking 机制分析
- Tcache Stashing Unlink Attack 原理与利用
