前言
本文重点关注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安全机制演变与对应绕过手法
tcache核心思想
首先不得不提的是,“快”,贯穿整个tcache机制之中——这决定了它的一些安全特性。
这使得刚开始2.26版本下突然引入的tcache没有任何安全检查,在后面的补丁中,也只是做了必要的、不太影响性能的安全检查。
tcache适用于非large bin大小的堆内存的管理,使用单链表,fd指针,
每当有新的内存大小被申请,其会创造一个新的入口点(entry)与其大小关联起来,以后申请和释放no large大小的块都会首先经过tcache内部指定大小的入口点的堆块遍历,查找有无合适的块,其中,这个大小通过以下方式来计算
下面根据glibc版本的演变过程作为时间轴来看看tcache安全机制的变化:
早期型tcache(glibc 2.26-2.27)
https://github.com/bminor/glibc/blob/release/2.26/master/malloc/malloc.c
所有需要了解的部分都在malloc.c
早期tcache结构
画了一下图片来纵观就非常清晰了,这里可以看清tcache的内部结构是怎么样的
![图片描述] (upload/attach/202411/869206_9ZHPKB7NGN2W72P.webp)
其中,一些限制值已经在代码上有预先的定义,当然这些个值也可以在编译阶段进行修改
tcache_perthread_struct(2.26-2.27)
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_entry(2.26-2.27)
早期tcache的tcache_entry结构如下
tcache_entry实际上指向tcache堆空闲块的fd位置,所以next其实跟fd是一个位置,作用相同,指向该大小tcache下的下一空闲块
早期tcache原子行为
首先了解一下tcache的最基础的行为,取用tcache和放入tcache,这两个行为的定义直接决定了tcache的安全机制是否足够安全
tcache_put(2.26-2.27)
这是tcache的原子行为之一:放入tcache
仅仅检查了所操作的tc_idx是否比限制值TCACHE_MAX_BINS更大,然后使用了头插法(O(1))来加入新的tcache_entry节点,这样比尾插法(O(n))更快
然后更新对应count[tc_idx]
tcache_get(2.26-2.27)
这是tcache的原子行为之二:取用tcache
同样,仅仅检查了所操作的tc_idx是否比限制值TCACHE_MAX_BINS更大
然后更新对应count[tc_idx]
这里两个原子行为的问题就是,tcache太追求速度了,舍弃了一切的安全机制,不做任何检查,导致极易进行double free,或者poisoning(其实就是溢出)等攻击
实际上后期正是在这里做了一点文章,加入了安全机制
中期tcache(2.28<=glibc <=2.33)
读罢早期tcache,其实最大的问题就是没有任何检查给攻击者造成麻烦,但是矛盾就是,这本身是一种强调速度的结构,如果加入安全机制比较消耗性能,那就失去了这个结构的存在意义。所以,现今tcache引入了一种key检验的机制,目的是在尽可能低的性能开销情况下,给没有获得信息泄露能力的攻击者造成一定麻烦
https://github.com/bminor/glibc/blob/release/2.28/master/malloc/malloc.c
总的来说,中期直到现今的结构大体上是差不多的,示意图如下
中期tcache结构(2.28<=glibc <=2.33)
tcache_perthread_struct(2.28<=glibc <=2.33)
更换了counts的类型,从char数组变为无符号整形,也许是预防了一手类型混淆,而且本来就应该这样写吧(?)
tcache_entry(2.28<=glibc <=2.33)
可以看到,原本对应于堆块的位置,放置了一个key值,这个key值就是2.28新增的安全机制的安全性保证所在
key实际上就是tcache_perthread_struct的地址,换言之,其在不同的tcache bin上是一样的值,一旦tcache_perthread_struct泄露就等于全部key的安全机制失效
而且,实际上next并没有做任何的校验,
中期tcache原子行为(tcache bin的取用和放入)
重点关注key的相关操作
tcache_put(2.28<=glibc <=2.33)
可以看到tcache的放入时会增添key
tcache_get(2.28<=glibc <=2.33)
可以看到tcache的取出,置空了key,以免在tcache堆重叠的情况下造成key泄露
key如何于free过程进行操作验证
如果一个chunk不是由mmap(申请128KB或更大的块)分配得到,就会调用_int_free进行释放
其实就是说,fastbin,tcache bin,small large bin,unsorted bin这些都是归_int_free管的
话不多说,跟进到_int_free()
...省略部分代码...
然后,代码经过一个关于 USE_TCACHE 的if判断,就会进入这里
请先默读一句话:“在tcache这里,答错key有奖励,答对key有惩罚”
_int_free把tcache bin跟一般bins混在一起,且使用操作块的key是否正确,来判断要不要跳入验证逻辑,所以其double free防御机制只会在key正确时启动
可以看到作者使用了unlikely来引导了glibc的分支预测来提高性能,这是因为很多情况下确实是 USE_TCACHE=1跳进来这里,但很多时候都不是tcache下的那些操作,所以要给它们更侧重的性能优化,实际上这被认为是一种剪枝算法,其确实在性能近乎不受影响的代价下,最大可能地给攻击者造成麻烦。
但值得一提的是,其e->key有极低概率被其它chunk的bk值刚好相等,导致进入该检查并触发double free的检查,这样的话会造成性能上的缓慢,但毕竟概率极低,可以接受
有好些文章里说这个e->key条件下的绕过double free很难,但是ta们全都错了,只要e->key不正确,就等于不需要进行那些判断了,而且这也并不会影响tcache_put的执行
这里有个逻辑问题
只要我们的改写再稍微溢出一点点,改到tcache bin结构内比fd稍高地址的bk部分,即存放key的部分,
改成不一样的key,就能轻松绕过判断逻辑继续double free,(这里让我一度怀疑是不是看漏了什么,事实如此)
Double free检测绕过(适用至今!)
能直接改key,就等于绕过double free检测了,这种方法就不细说啦
当然,如果没有对key的直接改写能力(比如,受限的溢出长度?),还可以考虑以下两种方法:
通过覆盖free chunk的size误导tc_idx生成
(更少的溢出长度)改变被 free 的堆块的大小,于是在以上遍历时可以误导tc_idx的计算,从而使tchache进入另一entry入口,这样就能避开double free检查。
回顾tc_idx的计算方法
其算法只有一句话:
式中,
x指代需要分配的大小
MALLOC_ALIGNMENT为各chunk的大小单位,MALLOC_ALIGNMENT在64位下是16字节,在32位下是8字节
而MINSIZE在64位下是8字节,32位下是4字节
实际上我们可以通过对请求大小的伪造,从而误导tc_idx的指向,以让double free检测检查不到任何重复的块(因为在别的chunk大小的entry处进行遍历)
House of botcake:free#1 unsortedbin(无key)->#2 tcache(有key)
House of botcake,简单来说就是抓住了tcache的一个特点:它只管tcache本身的double free安全,对于unsorted bin的块,因为其bk字段与key对不上,所以不会被检查
据此可以创造堆重叠的时机
通常的利用思路就是,填充完 tcache bin 链表后,然后把一个chunkA free到 unsorted bin 中,然后把这一个chunkA 上面紧邻的chunkB free掉,这样A、B就会合并,unsorted bin中的fd指针就从chunkA 的fd指针,变成了chunkB 的fd指针,之后我们先申请一个chunk 在tcache bin中给chunk A 留下空间,利用 House of Botcake 的原理(指unsorted bin的key不正确所以不需要接受double free检测)再free chunkA, 这时候chunk A 已经double free 了,然后我们可以在unsoreted bin中申请一个比较大的空间,通过chunkB、chunkA 的相邻来改变chunkA 的fd指针
读文字始终有些干巴巴的感觉诶,那就稍微画个示意图吧,方便直接看懂
其实还是答错key反而pass掉double free检测的这种设计逻辑太离谱了...Oops
2.32引入的safe-linking机制
异或加密是很快的,这受到了ptmalloc作者的青睐,并且用于指针的加解密以防直接的泄露,这对tcache poisoning,即对tcache的next字段进行篡改的手法变得稍微麻烦了一些
https://github.com/bminor/glibc/blob/release/2.32/master/malloc/malloc.c
在2.32版本,ptmalloc引入了PROTECT_PTR,即保护指针的概念,其指针是被异或加密的,如果对系统的堆地址一无所知,将无法正确解读泄露的指针的真实值
tcache_put当然也引入了这一机制,其next指针(fd)将会与entry首块进行异或加密
safe-linking机制绕过
在新的entry被put到tcache的时候,其fd将会与0异或,换言之,没有被加密,利用这一点,可以轻松泄露heap地址
我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL,就是0 )进行与0的异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的fd字段,我们就可以获得位移>>12后的地址,即我们可以获得基址
其使用如下的方式进行fd指针的加密
pos即原来的position,原来的地址
而受保护指针生成时的ptr,是指定了entry下的首个tcache地址,根据头插法,其实就是最近一个加入的tcache bin
在一个entry的第一块,其tcache_chunk_last(该entry下的最后一个chunk的地址)=null,
所以这里,如果entry是新建的,就会导致异或加密的失效
((size_t) pos) >> 12) ^ 0
只要申请一个从未申请过的tcache,就能让entry入口处的tcache bin 的fd被写入未被加密的堆相关地址,左移12位即可恢复为原来fd位置的,相关联heap地址
由于heap的topchunk是brk方式分配的,其会与页进行对齐(0x1000),其十六进制的低三位(二进制的低12位)将不会随机化,所以加上固定的页内偏移即可利用
现今tcache(glibc>=2.34)
https://github.com/bminor/glibc/blob/release/2.34/master/malloc/malloc.c
其实没有太大变化,要说的话只有key的生成方式变化了
现今key如何生成(2.34<=glibc<=2.38+)
改变并不多,2.34开始,key值是随机生成的,也就是说知道tcache_perthread_struct的地址不等于知道key,仅此而已
现今key如何于free过程进行操作验证(2.34<=glibc<=2.38+)
完全与中期相同,所以即使key保护得再好,再怎么随机化,照样可以在_int_free上绕过其判断逻辑,使相关double free检查机制作废
现今fd指针异或加密(2.32<=glibc<=2.38+)
完全与中期相同,还是那样去泄露首个entry[tc_idx]指向tcache bin的fd,就能得到无加密的堆地址
总结
不难发现,“快”这一个字,贯穿整个tcache实现的细节
tcache bin机制为了保证性能,其无法应用过于严厉的检测,并且为了通过性能测试,还使用了不安全的剪枝方式,结果把不该剪掉的枝也剪掉了
而且tcache跟加密fd指针配合的不是很到位,估计也是性能问题
参考
pwn.college - Dynamic Allocator Misuse - tcache(很简洁明了的图片,帮助快速建立概念)
https://github.com/bminor/glibc/blob/release/2.XX/master/malloc/malloc.c
https://xz.aliyun.com/t/15000?time__1311=GqjxuiqeqDwxl2z30%3DDOWGCY3PGK0Xox
https://xz.aliyun.com/t/7292?time__1311=n4%2BxnD0Dy7i%3Dit3G%3DNDsA3rxmhlme5bkoO4D
https://tttang.com/archive/1362/
https://www.anquanke.com/post/id/236186
https://bbs.kanxue.com/thread-269145-1.htm
https://xz.aliyun.com/t/12653?time__1311=GqGxuDRCG%3Dd052x%2BxCqb4RxqAx6LkqT4D#toc-2
https://forum.butian.net/share/1709