Tcache Attack综述(libc-2.27.so)
Tcache Attack综述(libc-2.27.so)
参考资料:
PWN入门(3-15-1)-Tcache Attack综述(libc-2.27.so) (yuque.com)
Tcache Attack总结 - FreeBuf网络安全行业门户
附件下载:
链接: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/
修改后:
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_entry和tcache_perthead_struct。
1 |
|
- 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结构体的地址。
tcache_get()和tcache_put();
tcache有两个重要函数,分别为tcache_get()和tcache_put();
tcache_get()是将free掉的从tcache取出(malloc)
tcache_put()是将被free掉的chunk放入tcache单向链表中(free)
1 | * Caller must ensure that we know tc_idx is valid and there's room |
这两个函数会在_int_free和_libc_malloc的开头被调用,其中tcache_put所请求的分配大小不大于0x408并且当给定大小的tcache bin未满时调用。一个tcache bin中的最大块数mp_.tcache_count是7,具体代码如下面所示:
1 |
再来看一下tcache_get()源码:
1 | static __always_inline void * |
会调用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 |
|
pwngdb调试
首先在代码第9行下断,执行程序,观察内存情况:
1 | pwndbg> heap |
m的值是随机数,没影响
在第11行下断,循环单步调试:
第一次
1 | pwndbg> x/16gx 0x601000 |
第二次
1 | pwndbg> x/16gx 0x601000 |
第三次
1 | pwndbg> x/16gx 0x601000 |
第四次
1 | pwndbg> x/16gx 0x601000 |
…….
第十次
1 | pwndbg> x/16gx 0x601000 |
在程序进行第7次free之后,tcache_perthread_struct中的指针所指向的地址不在变化,这是因为free 7次之后tcache中已经放满,故指针地址不会再变化。
从上面可以得出一个结论:当程序回收属于fastbin的堆块时,若tcache未满,程序会将free掉的chunk优先放入tcache中。
查看堆区内存情况:
1 | pwndbg> x/16gx 0x601000 |
执行完最后malloc循环得出结论
1 | 第一次malloc会使用chunk7 |
再结合一下前面放入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是多少,地址都是可控的。