本文重点关注tcache本身的结构、取用放入的原子操作,以及其free安全机制的演变过程,
大概分水岭在于
2.26(tcache出现)
2.28(_int_free引入key防止双重释放)
2.32(PROTECT_PTR的引入波及tcache利用)
2.34(使得key随机生成,而非tcache_perthread_struct的地址)
在这个过程中,可以看到ptmalloc力保tcache的性能,导致没有做到对堆利用的完全封堵
请见谅,本文对于tcache针对各sizebin的具体操作,将不会表现太多细节,因为如果要关注安全性,其实关注它们的底层一点的安全机制即可
首先不得不提的是,“快”,贯穿整个tcache机制之中——这决定了它的一些安全特性。
这使得刚开始2.26版本下突然引入的tcache没有任何安全检查,在后面的补丁中,也只是做了必要的、不太影响性能的安全检查。
tcache适用于非large bin大小的堆内存的管理,使用单链表,fd指针,
每当有新的内存大小被申请,其会创造一个新的入口点(entry)与其大小关联起来,以后申请和释放no large大小的块都会首先经过tcache内部指定大小的入口点的堆块遍历,查找有无合适的块,其中,这个大小通过以下方式来计算
下面根据glibc版本的演变过程作为时间轴来看看tcache安全机制的变化:
https://github.com/bminor/glibc/blob/release/2.26/master/malloc/malloc.c
所有需要了解的部分都在malloc.c
画了一下图片来纵观就非常清晰了,这里可以看清tcache的内部结构是怎么样的
![图片描述] (upload/attach/202411/869206_9ZHPKB7NGN2W72P.webp)
其中,一些限制值已经在代码上有预先的定义,当然这些个值也可以在编译阶段进行修改
tcache_perthread_struct,顾名思义,是每个线程都维护一个的tcache结构体,可以说其就是整个tcache的主干结构体
其维护了两个数组,实际上,这两个数组的每个位置,是一一对应的关系:一个负责计数,另一个负责管理tcache,一般使用tc_idx(tcache_index)这一名称作为数组索引
而每个索引tc_idx,其实对应的是不同大小的tcache bins,比如16字节大小的各个tcaches,32字节大小的各个tcaches,64字节大小的各个tcaches,会被分门别类地摆放到各自的entries[tc_idx]之下
其分门别类的方式也比较特别,实际上tc_idx是由csize2tidx()方法生成的,其参数tbytes其实就是用户请求内存的大小
其算法只有一句话:
式中,
x指代需要分配的大小
MALLOC_ALIGNMENT为各chunk的大小单位,MALLOC_ALIGNMENT在64位下是16字节,在32位下是8字节
而MINSIZE在64位下是8字节,32位下是4字节
以下注释解释了MALLOC_ALIGNMENT的计算结果
counts负责计数,计算的数目是每个索引之下的tcache bins数目
而entryies负责保存"入口",其指向某大小的首个tcache bins
值得一提的是,这所有的内容其实都是在堆上的,tcache_perthread_struct则更是在top_chunk附近的首个位置
早期tcache的tcache_entry结构如下
tcache_entry实际上指向tcache堆空闲块的fd位置,所以next其实跟fd是一个位置,作用相同,指向该大小tcache下的下一空闲块
首先了解一下tcache的最基础的行为,取用tcache和放入tcache,这两个行为的定义直接决定了tcache的安全机制是否足够安全
这是tcache的原子行为之一:放入tcache
仅仅检查了所操作的tc_idx是否比限制值TCACHE_MAX_BINS更大,然后使用了头插法(O(1))来加入新的tcache_entry节点,这样比尾插法(O(n))更快
然后更新对应count[tc_idx]
这是tcache的原子行为之二:取用tcache
同样,仅仅检查了所操作的tc_idx是否比限制值TCACHE_MAX_BINS更大
然后更新对应count[tc_idx]
这里两个原子行为的问题就是,tcache太追求速度了,舍弃了一切的安全机制,不做任何检查,导致极易进行double free,或者poisoning(其实就是溢出)等攻击
实际上后期正是在这里做了一点文章,加入了安全机制
读罢早期tcache,其实最大的问题就是没有任何检查给攻击者造成麻烦,但是矛盾就是,这本身是一种强调速度的结构,如果加入安全机制比较消耗性能,那就失去了这个结构的存在意义。所以,现今tcache引入了一种key检验的机制,目的是在尽可能低的性能开销情况下,给没有获得信息泄露能力的攻击者造成一定麻烦
https://github.com/bminor/glibc/blob/release/2.28/master/malloc/malloc.c
总的来说,中期直到现今的结构大体上是差不多的,示意图如下
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)