浅析tcache安全机制演进过程与绕过手法

前言

本文重点关注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的内部结构是怎么样的

其中,一些限制值已经在代码上有预先的定义,当然这些个值也可以在编译阶段进行修改

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

评论

  1. Nop
    2 月前
    2024-8-27 14:36:16

    看不懂 撤!

    • 博主
      Nop
      2 月前
      2024-8-28 19:36:04

      我也看不懂车联网呜呜,带带小迷弟好吗

  2. muyiGin
    2 天前
    2024-10-17 19:34:06

    天呐,根据版本前中后期分类,讲的真是太棒了,我真是太幸运了,能看到这篇文章。一直东一棒西一棒的学,根本不知道什么版本开始引入某些机制,真心感谢博主。
    by the way,这些是博主学校课堂上学的吗?我觉得我现在跟着wiki学的有点杂(并且示例的版本都好旧),不知道博主的学习路径,如果能借鉴一下就好了~(∠・ω< )⌒☆

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
粤ICP备20015830号