Tcache Attack综述(libc-2.27.so)

参考资料:

PWN入门(3-15-1)-Tcache Attack综述(libc-2.27.so) (yuque.com)

Tcache Attack总结 - FreeBuf网络安全行业门户

tcache (yuque.com)

附件下载:

链接:https://pan.baidu.com/s/1VHwTJv26I381RU8e0jrjnw
提取码:ki7n
–来自百度网盘超级会员V3的分享

实验环境

因为有点懒,本机只有ubuntu16.04和22.04的版本,虽然22版本的也有tcache机制,但是和本章要讲的还是略有差别,所以我利用patchelf修改了一下glibc版本。

具体步骤可以参考https://zhuyuan1213.top/2023/08/12/glibc%E7%89%88%E6%9C%AC%E7%9A%84%E6%9B%B4%E6%8D%A2/

修改后:

image-20230812152651843

Tcache overview

tcache(全名thread local caching) 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术(see commit),目的是提升堆管理的性能。它为每个线程创建一个缓存(cache),从而实现无锁的分配算法,有不错的性能提升。lib-2.26【2.23以后】正式提供了该机制,并默认开启。

tcache类似于fastbin,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找。

tcache struct

tache有两个相关结构体:tcache_entrytcache_perthead_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#glibc-malloc.c源码
#if USE_TCACHE

/* 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;//指向的下一个chunk的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];//数组长度64,每个元素最大为0x7,仅占用一个字节(对应64个tcache链表)
tcache_entry *entries[TCACHE_MAX_BINS];//entries指针数组(对应64个tcache链表,tcache bin中最大为0x400字节)每一个指针指向的是对应tcache_entry结构体的地址。
} tcache_perthread_struct;

static __thread bool tcache_shutting_down = false;
static __thread tcache_perthread_struct *tcache = NULL;
  • tcache_entry结构体中的值是一个指向tcache_entry结构体的指针,是一个单链表结构。
  • tcache_perthead_struct结构体是用来管理tcache链表的。其中的count是一个字节数组,大小共64字节,对应64个tcache单向链表。每个链表最多7个节点(chunk),chunk的大小在32bit上是12到512(8byte递增);在64bits上是24(0x18)到1024(0x400)(16bytes递增)。count中每一个字节表示的是tcache每一个链表中有多少元素。entries是一个指针数组,数组中共64个元素,对应64个tcache链表,因此tcache bin中最大为0x400字节,每一个指针指向的是对应tcache_entry结构体的地址。

image.png

tcache_get()和tcache_put();

tcache有两个重要函数,分别为tcache_get()和tcache_put();

tcache_get()是将free掉的从tcache取出(malloc)

tcache_put()是将被free掉的chunk放入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
* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
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;
}

这两个函数会在_int_free和_libc_malloc的开头被调用,其中tcache_put所请求的分配大小不大于0x408并且当给定大小的tcache bin未满时调用。一个tcache bin中的最大块数mp_.tcache_count是7,具体代码如下面所示:

1
2
# define TCACHE_FILL_COUNT 7
#endif

再来看一下tcache_get()源码:

1
2
3
4
5
6
7
8
9
10
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];//根据索引找到tcache链表的头指针
assert (tc_idx < TCACHE_MAX_BINS);//tcache计数器检查1
assert (tcache->entries[tc_idx] > 0);//tcache计数器检查2
tcache->entries[tc_idx] = e->next;//将chunk取出
--(tcache->counts[tc_idx]);//tcache计数器减一
return (void *) e;
}

会调用tcache_get函数的情形:

  • 在调用malloc_hook之后,_int_malloc之前,如果tcache中有合适的chunk,那么就从tcache中取出:
  • 遍历完unsorted bin后,若tcachebin中有对应大小的chunk,从tcache中取出:
  • 遍历unsorted bin时,大小不匹配的chunk会被放入对应的bins,若达到tcache_unsorted_limit限制且之前已经存入过chunk则在此时取出(默认无限制)

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

tcache为空时:

  • 首先,申请的内存块符合fastbin大小时并且在fastbin内找到可用的空闲块时,会把该fastbin链上的其他内存块放入tcache 中。
  • 其次,申请的内存块符合smallbin大小时并且在smallbin 内找到可用的空闲块时,会把该smallbin 链上的其他内存块放入tcache 中。
  • 当在unsorted bin链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到tcache 中,继续处理。

上述3种情况将chunk放入tcache中后,在将符合申请条件的chunk返回利用。

tcache和fastbin的不同

来说一下tcache和fastbin链表的异同点:

  • tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针。
  • tcachebin的链表指针是指向下一个chunk的fd字段,fastbin的链表指针是指向下一个chunk的pre_size字段
  • 在_int_free中,最开始就先检查chunk的size是否落在了tcache的范围内,且对应的tcache未满。将其放入tcache中。
  • 在_int_malloc中,如果从fastbin中取出了一个块,那么会把剩余的块放入tcache中至填满tcache(smallbin中也是一样)
  • 如果进入了unsortbin中,且chunk的size和当前申请的大小精确匹配,那么在tcache未满的情况下会将其放入到tcachebin中。

举例验证

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<stdlib.h>
int main(){
void *p[10]={0};
int i,j,m;
for(i = 0 ;i < 10 ; i++){
p[i]=malloc(0x10);
}
for(j = 0 ;j < 10 ; j++){
free(p[j]);
p[j]=NULL;
}
for(m = 0 ; m < 5;m++){
p[m]=malloc(0x10);
}
return 0;
}

pwngdb调试

首先在代码第9行下断,执行程序,观察内存情况:

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
pwndbg> heap
0x601000 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 593,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x601250 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601270 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601290 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x6012b0 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x6012d0 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x6012f0 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601310 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601330 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601350 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x601370 FASTBIN {
mchunk_prev_size = 0,
mchunk_size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20c71
}
0x601390 PREV_INUSE {
mchunk_prev_size = 0,
mchunk_size = 134257,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251 #tcache_perthread_struct
......(省略内容均为空)
0x601240: 0x0000000000000000 0x0000000000000000
0x601250: 0x0000000000000000 0x0000000000000021 #chunk1
0x601260: 0x0000000000000000 0x0000000000000000
0x601270: 0x0000000000000000 0x0000000000000021 #chunk2
0x601280: 0x0000000000000000 0x0000000000000000
0x601290: 0x0000000000000000 0x0000000000000021 #chunk3
0x6012a0: 0x0000000000000000 0x0000000000000000
0x6012b0: 0x0000000000000000 0x0000000000000021 #chunk4
0x6012c0: 0x0000000000000000 0x0000000000000000
0x6012d0: 0x0000000000000000 0x0000000000000021 #chunk5
0x6012e0: 0x0000000000000000 0x0000000000000000
0x6012f0: 0x0000000000000000 0x0000000000000021 #chunk6
0x601300: 0x0000000000000000 0x0000000000000000
0x601310: 0x0000000000000000 0x0000000000000021 #chunk7
0x601320: 0x0000000000000000 0x0000000000000000
0x601330: 0x0000000000000000 0x0000000000000021 #chunk8
0x601340: 0x0000000000000000 0x0000000000000000
0x601350: 0x0000000000000000 0x0000000000000021 #chunk9
0x601360: 0x0000000000000000 0x0000000000000000
0x601370: 0x0000000000000000 0x0000000000000021 #chunk10
0x601380: 0x0000000000000000 0x0000000000000000
0x601390: 0x0000000000000000 0x0000000000020c71 #top_chunk

image-20230812171016033

m的值是随机数,没影响

在第11行下断,循环单步调试:

第一次

image-20230812171306643

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251
0x601010: 0x0000000000000001 0x0000000000000000
#tcache中chunk数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x0000000000601260 0x0000000000000000
#指向chunk_data
0x601060: 0x0000000000000000 0x0000000000000000
0x601070: 0x0000000000000000 0x0000000000000000

第二次

image-20230812171337108

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251
0x601010: 0x0000000000000002 0x0000000000000000
#tcache中chunk数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x0000000000601280 0x0000000000000000
#指向chunk_data
0x601060: 0x0000000000000000 0x0000000000000000
0x601070: 0x0000000000000000 0x0000000000000000

第三次

image-20230812171415701

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251
0x601010: 0x0000000000000003 0x0000000000000000
#tcache中chunk数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x00000000006012a0 0x0000000000000000
#指向chunk_data
0x601060: 0x0000000000000000 0x0000000000000000
0x601070: 0x0000000000000000 0x0000000000000000

第四次

image-20230812171437621

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251
0x601010: 0x0000000000000004 0x0000000000000000
#tcache中chunk数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x00000000006012c0 0x0000000000000000
#指向chunk_data
0x601060: 0x0000000000000000 0x0000000000000000
0x601070: 0x0000000000000000 0x0000000000000000

…….

第十次

image-20230812171645742

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251
0x601010: 0x0000000000000007 0x0000000000000000
#tcache中chunk数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x0000000000601320 0x0000000000000000
#指向chunk_data
0x601060: 0x0000000000000000 0x0000000000000000
0x601070: 0x0000000000000000 0x0000000000000000

在程序进行第7次free之后,tcache_perthread_struct中的指针所指向的地址不在变化,这是因为free 7次之后tcache中已经放满,故指针地址不会再变化。

从上面可以得出一个结论:当程序回收属于fastbin的堆块时,若tcache未满,程序会将free掉的chunk优先放入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
pwndbg> x/16gx 0x601000
0x601000: 0x0000000000000000 0x0000000000000251 #tcache_perthread_struct
0x601010: 0x0000000000000007 0x0000000000000000
#tcache中chunk的数量
0x601020: 0x0000000000000000 0x0000000000000000
0x601030: 0x0000000000000000 0x0000000000000000
0x601040: 0x0000000000000000 0x0000000000000000
0x601050: 0x0000000000601320 0x0000000000000000
......(省略内容均为空)
0x601240: 0x0000000000000000 0x0000000000000000
0x601250: 0x0000000000000000 0x0000000000000021 #chunk1
0x601260: 0x0000000000000000 0x0000000000000000
0x601270: 0x0000000000000000 0x0000000000000021 #chunk2
0x601280: 0x0000000000601260 0x0000000000000000
0x601290: 0x0000000000000000 0x0000000000000021 #chunk3
0x6012a0: 0x0000000000601280 0x0000000000000000
0x6012b0: 0x0000000000000000 0x0000000000000021 #chunk4
0x6012c0: 0x00000000006012a0 0x0000000000000000
0x6012d0: 0x0000000000000000 0x0000000000000021 #chunk5
0x6012e0: 0x00000000006012c0 0x0000000000000000
0x6012f0: 0x0000000000000000 0x0000000000000021 #chunk6
0x601300: 0x00000000006012e0 0x0000000000000000
0x601310: 0x0000000000000000 0x0000000000000021 #chunk7
0x601320: 0x0000000000601300 0x0000000000000000
0x601330: 0x0000000000000000 0x0000000000000021 #chunk8
0x601340: 0x0000000000000000 0x0000000000000000
0x601350: 0x0000000000000000 0x0000000000000021 #chunk9
0x601360: 0x0000000000601330 0x0000000000000000
0x601370: 0x0000000000000000 0x0000000000000021 #chunk10
0x601380: 0x0000000000601350 0x0000000000000000
0x601390: 0x0000000000000000 0x0000000000020c71 #top_chunk

执行完最后malloc循环得出结论

1
2
3
4
5
第一次malloc会使用chunk7
第二次malloc会使用chunk6
......
第五次malloc会使用chunk3

再结合一下前面放入tcache中chunk的顺序,可以总结出结论:

当申请属于fastbin大小的chunk时,若tcache中仍有chunk,首先将tcache末尾的chunk取出

tache的特性:先进后出(后进先出)

tcache attack

tcache dup(重复利用)

  • fastbin中的double free利用,我们需要构成a->b->a这种形式的free’d链,而在tcache中,由于不会检查top,直接可以构成a->a*这种free’d链。利用更方便。

tcahe_house_of_spirit

  • 与fastbin的house_of_spirit类似。free掉伪造的chunk,再次malloc获得可操作的地址。但是同样的,这里更简单,free的时候不会对size做前后堆块的安全检查,所以只需要size满足对齐就可以成功free掉伪造的chunk(其实就是一个地址)。

tcache_overlapping_chunks(重叠块)

  • 可以说和house of spirit是一个原因,由于size的不安全检查,我们可以修改将被free的chunk的size改为一个较大的值(将别的chunk包含进来),再次分配就会得到一个包含了另一个chunk的大chunk。同样的道理,也可以改写pre_size向前overlapping。

tcache_poisoning

  • 这个着眼于tcache新的结构,这里的next指针其实相当于fastbin下的fd指针的作用(而且没有很多的检查),将已经在tcache链表中的chunk的fd改写到目的地址,就可以malloc合适的size得到控制权。需注意,tcache dup和poisoning其实都要求可以use after free,也就是free并没有置null。

tcache_perthread_corruption

  • tcache_perthread_corruption是整个tcache的管理结构,如果能控制这个结构体,那么无论malloc的size是多少,地址都是可控的。