tcache学习
tcache简介
tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术(see commit),目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。
glibc用USE_TCACHE
条件开启 tcache 机制以及编译相关代码。
示例源码均来自glibc2.26,基本都是在glibc-2.26/malloc/malloc.c
中
相关数据结构
宏定义
1 |
|
TCACHE_MAX_BINS
是最大条目数,每个条目将尺寸相同的 tcache chunk 连接成一个单向链表,FILO,默认为64;
MAX_TCACHE_SIZE
是 tcache chunk 的最大尺寸,chunk 的大小在 64 位机器上以 16 字节递增,从 24 到 1032 字节。32 位机器上则是以 8 字节递增,从 12 到 512 字节。所以 tcache bin 只用于存放 non-large 的 chunk;
tidx2usize(idx)
用于计算tcache chunk size,传入下标idx,返回size;
csize2tidx(x)
用于将chuncksize转化成idx;
usize2tidx(x)
用于将提供给用户的size转化成idx;
TCACHE_FILL_COUNT
表示每条链表的最大长度,默认为7
结构体tcache_entry和tcache_perthread_struct
tcache 引入了两个新的数据结构tcache_entry
和tcache_perthread_struct
1 |
|
tcache_entry
就是tcache链表的节点,tcache_perthread_struct
中包含一个指针数组entrys[TCACHE_MAX_BINS]
,用于存放(默认)64个 tcache bins 链表头的地址,还有一个counts[TCACHE_MAX_BINS]
数组用于存放每条链表的长度,因为最长为7,所以用 char 类型足够。
需要注意的是和其他 bins 不同,tcache_entry
的 next 指针在tcache_entry
结构体的开始,而其他 bins 中类似功能的 fd 指针的位置在chunk+0x10
的位置(64bit)。
静态变量tcache_shutting_down
初始化为0表示tcache没有关闭,默认开启;
静态变量tcache
初始化为NULL,一开始 tcache 没有初始化,在第一次 malloc 初始化堆时才初始化 tcache 并赋值到此处。
相关函数
tcache_init
tcache 初始化函数
1 | static void |
可以看出 tcache 是存在于堆中的一块大 chunk,执行初始化函数时之前置NULL的静态变量tcache
被赋值成这里 malloc 出来的大 chunk,在64位机器中 tcache size 一般为0x291
。
MAYBE_INIT_TCACHE
在__libc_malloc
中被调用:
1 |
|
每次malloc都会执行MAYBE_INIT_TCACHE
,但是因为里面的判断,只有第一次才会执行tcache_init
tcache_put
将 free 的 chunk 按照 size 加入到特定的 tcache bin 中
1 | /* Caller must ensure that we know tc_idx is valid and there's room |
没有消除下一个 chunk 的 P 位
tcache_put
会在_int_free
中被调用1
2
3
4
5
6
7
8
9
10
11
12
13
{
size_t tc_idx = csize2tidx (size); //获取idx
if (tcache //判断tcache是否初始化
&& tc_idx < mp_.tcache_bins //判断尺寸
&& tcache->counts[tc_idx] < mp_.tcache_count) //判断tcache bin是否装满
{
tcache_put (p, tc_idx);
return;
}
}仅判断如果长度在范围内且对应 tcache bin 未满,就执行
tcache_put
将 free 的 chunk 加入该链表。_int_malloc
中如果从 fastbin 中成功返回了一个需要的 chunk,那么对应 fastbin 中的其他 chunk 会被放进相应的 tcache bin 中,直到上限。需要注意的是两者都是 FIFO 策略,所以 chunks 在 tcache bin 的顺序和在 fastbin 中的顺序是反过来的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins) //判断tcache是否初始化以及尺寸
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (pp = *fb) != NULL) //判断tcache bin未满且fast bin不空
{
REMOVE_FB (fb, tc_victim, pp); //取出fastbin头
if (tc_victim != 0)
{
tcache_put (tc_victim, tc_idx); //放入tcache bin
}
}
}smallbin 与 fastbin 类似,从中返回 chunk 时会把剩余 chunk 加入到 tcache bin 中直到上限
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
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin) //判断smallbin不空
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim); //取出smallbin尾
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx); //加入tcache bin头
}
}
}当fastbins和smallbins都不能满足条件就会在unsortedbins中寻找,在unsortedbin bin链上寻找时,每一个符合要求的 chunk 都会优先被放入 tcache,而不是直接返回(除非tcache被装满)。寻找结束后,tcache 会返回其中一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Fill cache first, return to user only if cache fills.
We may return one of these chunks later. */
if (tcache_nb
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (victim, tc_idx);
return_cached = 1;
continue;
}
else
{
#endif
tcache_get
从指定 idx 的 tcache bin 头部取出一个 chunk
1 | /* Caller must ensure that we know tc_idx is valid and there's |
tcache_get
会在__libc_malloc
调用_int_malloc
之前被调用(在之前展示过的初始化之后),所以malloc 时时若条件满足会优先从tcache中返回一个chunk。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes = request2size (bytes);
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins //尺寸
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache //初始化
&& tcache->entries[tc_idx] != NULL) //链表头非空
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;此处仅仅检查了
tcache->entries[tc_idx] != NULL
,可能存在 uaf 或者堆溢出漏洞可以设置处于 tcache bin 中 chunk 的数据来实现任意位置 malloc ,这周攻击方式叫 tcache poisoning。在 unsortedbin 中寻找 chunk 时,如果在 tcache 中放入 unsortedbin chunk 达到上限,则会直接返回最后一个 chunk。
1
2
3
4
5
6
7
8
9
10
11
/* If we've processed as many chunks as we're allowed while
filling the cache, return one of the cached ones. */
++tcache_unsorted_count;
if (return_cached
&& mp_.tcache_unsorted_limit > 0
&& tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get (tc_idx);
}但是默认情况下没有限制,这段代码也不会执行:
1
.tcache_unsorted_limit = 0 /* No limit. */
在 unsortedbin 中寻找 chunk 时,如果之前曾放入过 tcache 块,则会取出一个并返回。
1
2
3
4
5
6
7
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get (tc_idx);
}
tcache bins 中的 chunk 不会被合并,无论是相邻 chunk 还是 top chunk。
tcache_thread_freeres
1 | static void __attribute__ ((section ("__libc_thread_freeres_fn"))) |
利用
tcache 分配和释放都只检查了 tcache 相关结构体的一些数据,没有前后检查等其他的检查,而且 tcache 相关代码比其他 bins 更优先,原先的很多检查机制都不会执行,所以 tcache 技术虽然提升了效率,但安全性却很差。
tcache_dup
1 | //代码嫖自how to heap |
1 | $ ./a.out |
tcache_dup 比 fastbin_dup 更简单,因为 tcache 没有连续两次 free 同一个指针的检查,就不用在中间 free 一个别的指针绕过,而且 tcache 不会与 top chunk 合并,还有 tcache 的范围比 fastbin 更大,会有更大的适用空间。glibc中依然可用,glibc2.31不可用。
tcache_house_of_spirit
1 | //代码嫖自how to heap |
1 | $ ./a.out |
tcache 释放 chunk 时没有对前后 chunk 进行检验,只需要释放的 chunk 对齐就能释放到 tcache bins 中,而且同样因为 tcach 的范围比 fastbin 大,适用范围扩大到包括 snallbin 的大小。此漏洞在glibc2.31中依然可以利用。
tcache_overlapping_chunks
1 | //代码嫖自how to heap |
1 | $ ./a.out |
_int_free
中tcache_put
之前没有对 chunk 进行前后检查,可以直接修改 size 再将其 free 放进修改后size对应的 tcache bin 中,malloc 时也可以直接将其返回,相当于可以随意修改 size 造成堆块重叠。glibc2.31中依然可用。
tcache_poisoning
1 | ///代码嫖自how to heap |
1 | $ ./a.out |
通过修改 tcache bin 中 chunk 的 fd 指针,使其指向任意位置,同时也就修改了 tcache_entry 的 next 指针,malloc 时将从此指针指向的地址返回 chunk,__libc_malloc
中在tcache_get
之前只检查了tcache->entries[tc_idx] != NULL
,没有检查 count。
修改指针前:
1 | pwndbg> tcachebins |
count 为1,链表中也只有一个节点。
修改指针后:
1 | pwndbg> tcachebins |
可以看到虽然 count 依然为1,但链表中已经加入了我们的 target。
第一次 malloc 后:
1 | pwndbg> tcachebins |
count 减为0,但此时链表中还有 target。
第二次 malloc 后:
1 | pwndbg> tcachebins |
count 减成了-1,target 成功取出。
此漏洞在glibc2.27中依然可用,glibc2.31中已被修复,加入了对 count 的检查:
1 | //glibc-2.31/malloc/malloc.c __libc_malloc() |