Glibc中堆管理的变化
前言
在学pwn的道路上,我们大多从linux入手,从栈到堆,各种漏洞利用,都和Glibc或多或少打过交道。我的堆入门应该和很多人一样是从libc2.23开始的,之后又经历了各种libc版本的变化,随着现在的pwn题越来越与时俱进,我们会逐渐接触更新的libc版本,因此,我们必须知道,Glibc中堆管理变化了什么,从安全角度,我们的得失又是什么呢?从libc2.27开始,我们聊一聊Glibc中堆管理的漏洞利用得失。
关键字: CTF
pwn
新版本libc
libc 2.27
libc 2.29
libc 2.30
堆溢出
GLibc2.27
Glibc2.23我就不想多说了,感兴趣的朋友可以学一学pwn相关的堆漏洞利用知识,网上现在总结的也算是比较多。我就不赘述了。从Glibc2.27开始,发生了很多有趣的地方,我们一起聊一聊。
Tcache
Tcache可是说是Glibc2.27中一个大的改变,其实Tcache的引入是从Glibc2.26开始的。但是(以下个人见解)Linux中比较受欢迎的发行版,ubuntu 18.04中的libc版本是2.27,再加上很多发行版都是2.27版本,所以,我们常见的pwn题也就在这种环境下编译开发了,因此,我们直接说说2.27版本,跳过2.26版本。
我认为Tcache使得漏洞利用变简单了,其得失我总结了一下:
- 漏洞利用最后一哆嗦,特别简单暴力
- Tcache的管理结构在堆上,比main_arena好搞一点,毕竟Libc地址一般比堆地址难搞到
- Tcache有时候使得泄露Libc地址变得困难
这里我说下Tcache的机制,tcache就是一个为了内存分配速度而存在的机制,当size不大(这个程度后面讲)堆块free后,不会直接进入各种bin,而是进入tcache,如果下次需要该大小内存,直接讲tcache分配出去,是不是感觉和fastbin蛮像的,但是其size的范围比fastbin大多了,他有64个bin链数组,也就是(64+1)*size_sz*2
,在64位系统中就是0x410大小,有图有真相:
也就是说,在64位情况下,tcache可以接受0x20~0x410大小的堆块。
Tcache poisoning
那么Tcache对漏洞利用来说,不像fastbin attack一样,需要寻找合适的size了,在2.27的环境下是可以直接做到任意地址写的,这一点非常nice, 这种利用方法,在也被叫做tcache poisoning。同时,在double free领域,Tcache可以直接double free,而不需要像fastbin那样,需要和链上上一个堆块不一样,也就是下面这个样子。
1 | /* |
还有一点不同,就是在Tcache中,fd指向的并不是堆头,而是堆内容,这一点也是需要我们注意的。
leak libc地址
单纯在堆中leak libc地址,一般是使用size大于fastbin范围的堆块,而在有tcache的情况下,这个变得相较之前困难,我将我目前用的比较多的方法总结如下:
1、申请8个大堆块,释放8个,这里堆块大小,大于fastbin范围,就是填满tcache,注意和top chunk的位置。
2、有double free的情况下,连续free 8次同一个堆块,这里堆块大小,大于fastbin范围,注意和top chunk的位置。
3、申请大堆块,大于0x410。
4、修改堆上的Tcache管理结构
大致就是以上几种方法,如果还有其他的想法,欢迎交流。
Tcache Stashing Unlink Attack
网上有很多人在分析这一漏洞的时候,都是基于libc2.29分析的,其实在libc2.27中,这一漏洞就已经存在了。
这里简单讲下,这是small bin 中的检查,即:__glibc_unlikely(bck->fd != victim)
1 | // 获取 small bin 中倒数第二个 chunk 。 |
我们来看看Tcache中的情况:
1 |
|
那么,我们需要注意2个地方,就是我在源码中标注的0和1。那么这两个地方由于没有任何检查,导致了两个问题,1、任意地址写libc地址,2、将任意地址放入tcache。
那么这段的逻辑是什么呢,简单来说,当我们从smallbin中申请了一个chunk后,会将此大小的tcache用smallbin里的堆块填满。
我们来看看什么时候,终止填入呢,两个条件:tcache->counts[tc_idx] >= mp_.tcache_count || (tc_victim = last (bin)) == bin
就是上述while循环中的相反的条件。也就是说,如果smallbin里没heap了或者tcache填满了,就不需要继续填充了,但是由于我们期望漏洞利用,所以需要改掉bck,这就导致(tc_victim = last (bin)) == bin
这个条件是很难达到的。所以,我们需要控制tcache中的数量,但是,这里又出现了一个矛盾,那就是如果Tcache不为空,就不会从smallbin中取出堆块。
所以,综上所述,只有绕过tcache的calloc能够符合这样的要求,那么,如果,我们想要任意地址写libc,就在tcache中留一个空间,如果期望任意地址放入tcache,就在tcache中留两个空间,同时,我们需要清楚,动手脚的small bin 应该是倒数第二个smallbin。
画个图示意一下:
将Chunk1的bk指向目标地址,再calloc一个0xa0大小的chunk,参照上述的目的,确定自己需要在Tcache中留几个heap。
Tcache结构破坏
这个其实没什么好说的,只是一个tips吧,tcache的管理结构在堆上,再加上tcache宽松的检查条件,其实有时候搞一搞这里还是蛮有意思的。
libc2.27中的东西基本就讲这些了,接下来就是libc2.29了
Glibc2.29
在2.27的基础上,我们看看2.29做了哪些改变:
Tcache的double free防护
首先是一个对漏洞利用者较为遗憾的改动,就是在tcache的结构体上,加了一个key。
在官方注释上,这一增加是为了检测tcache的double free,在2.27的libc中,tcache为了速度,几乎没有什么安全保护,这一机制会缓解部分漏洞利用。
那么,这一增加如何作用呢,我们可以看到,在tcache_put中,对这一结构体进行了赋值,赋值的内容就是定义的tcache_perthread_struct结构体tcache的地址,tcache就是通过这一函数来判断当前的heap是否在tcache中,当然,在tcache_get中,也会将其清理。同时在free中加了这么一段。
1 | if (__glibc_unlikely (e->key == tcache)) |
也就是说在free时,如果当前的chunk的bk位置是tcache这一地址,那么就会循环检测当前大小的tcache的链表,查看链表中是否存在当前的chunk。所以,想要double free前,记得先改一下bk。
unlink前操作
在free的时候,unlink前新加了一个检查,这个不太致命,注意绕过即可。
1 | /* consolidate backward */ |
unsortbin保护
不说了,unsortbin attack我先不用了,总可以了吧(含泪)。
1 | if (__glibc_unlikely (size <= 2 * SIZE_SZ) |
libc 2.30
那么到了libc2.30其实增加的东西也是不多了。
largebin attack
在largebin 中,加了这个,刚好对largbin的bk和bk_nextsize做出了限制。
那么在插入large bin时,就不能使用large bin Attack了(关于Largebin Attack的方法,可参照我之前文章http://pwn.sofr.website/2020/04/02/LargebinAttack/)
写在最后
文章首发于freebuf
如有错误欢迎指正:sofr@foxmail.com