tcache浅谈

Heap基础知识

[toc]

tcache attack

先略记录一下,以后可能会回来补充细节。

什么事tcache

这是glibc 2.26以后引入的一种为了加快内存分配速度的机制,但同时也产生了很多安全漏洞。由于ubuntu18.04已经开始使用glibc 2.27,因此ubuntu 18.04版本存在tcache机制的使用。由于tcache机制也逐渐进行了诸多更新,修复了部分漏洞,本文暂时站在tcache最初始的版本进行讲解。

若存在tcache机制,那么当一个非large chunkfree后,它不再会被直接添加到unsorted bin或者fast bin,而是被添加到对应的tcache bin中。tcache bin是一个单向链表结构,只有fd指针,结构可以说是非常简单。

同一个大小的tcache中只能存在7chunk(默认情况下)。因此,若你想将一个chunk申请到unsorted bin中,不妨申请8个属于unsorted binchunk,由此也可以使用unsorted bin leak来泄露libc基地址。

值得注意的是,tcache指向的直接是用户地址,而不是之前bin指向的是header的地址。

tcache perthread struct

对于tcacheglibc会在第一次申请堆块的时候创建一个tcache_perthread_struct的数据结构,同样存放在堆上。它的定义如下所示:

1
2
3
4
5
6
7
8
9
/* 每个线程都有一个这个数据结构,所以他才叫"perthread"。保持一个较小的整体大小是比较重要的。  */
// TCACHE_MAX_BINS的大小默认为64
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
// 在glibc2.26-glibc2.29中,counts的大小为1个字节,因此tcache_perthread_struct的大小为1*64 + 8*64 = 0x250(with header)
// 在glibc2.30及以上版本中,counts的大小为2个字节,因此tcache_perthread_struct的大小为2*64 + 8*64 = 0x290(with header)

tcache poisoning及其变迁

glibc2.27低版本

若存在tcache机制时,若申请一个属于tcache中的chunk,使用到的函数是tcache_get()函数,该函数在初始版本没有任何的安全机制,因此只需要简单地将某个tcache中的chunkfd指针修改为想要分配到的地方,即可在目标地址申请一个chunk

glibc2.27高版本-glibc2.29

glibc2.27的高版本开始,tcache chunkbk将不再为空,而是tcache_perthread_struct的用户地址。然而这和tcache poisoning没有任何关系,我们仍然可以没有任何限制地使用tcache poisoning attack

glibc2.31

glibc2.31glibc添加了对tcachecount的检查,即在对应indexcount小于等于0时,无法从该tcache中申请chunk。这意味着我们在tcache中只有一个chunk时修改其fd指针来完成攻击,而是至少需要两个chunk位于同一个tcache bin中。

总结:

  • 至少两个chunk位于同一个tcache bin(或者别的方式,总之有count检查)

glibc2.32-glibc2.38

自从glibc2.32开始,glibc引入了相当多对于tcache的检查。我们这里只说poisoning,不说double free,这一部分在后面。

首先,tcache bin会检查待申请的地址是否对齐,即末位是否为0。若不为0,则会报错。

其次,tcachefd指针将会被加密。假设要申请的地址为targetchunk的用户地址为address,则:

1
chunk->fd = target ^ (address >> 12);

值得注意的是,在glibc2.27高版本到glibc2.33,我们可以通过泄露bk的方式来获得堆地址。

总结:

  • 至少两个chunk位于同一个tcache bin(或者别的方式,总之有count检查)
  • 待申请地址必须对齐
  • chunk -> fd = target ^ (address >> 12)

tcache double free

若没有tcache的时候,double free不能简单地连续对一个chunk进行free两次这个机制略显复杂的话,那么tcache double free就显得单纯许多了。在初始版本下tcache的释放操作是使用的tcache_get()函数,该函数同样没有任何安全机制,因此可以简单地直接对一个chunk进行两次free,因此可以申请回该chunk,对其修改后再次申请,完成任意地址写/任意地址chunk分配的目的。

高版本

glibc2.27高版本开始简单的tcache double free已经成为了历史。

patch方式为tcache chunkbk指针处将会有一个key,对于每一个释放的chunk,若其bk指针等于其key,说明该chunk属于tcache,便会循环tcache来遍历检查其是否已经位于tcache。因此若你能修改其bk指针为任意值(只要不等于key),也可以进行double free。(或者你可以尝试house of botcake

  • glibc2.27高版本-glibc2.33tcachekeytcache_perthread_struct的用户地址。

  • glibc2.33-glibc2.38tcachekey为随机数。

tcache house of spirit

fastbinhouse of spirit是相当类似的,不同的是,tcachehouse of spirit更加简单,可以直接在任意地方伪造一个chunk然后进行freefast binhouse of spirit还需要控制要free的下一个chunksize域。

tcache stash unlink可以达成两个目的:

  • 任意地址申请一个fake chunk
  • 往任意地址写一个main_arena地址

看起来就像fastbin attackunsortedbin attack的结合,威力很强,但是同时利用条件也非常苛刻:

  • 需要保证同一种大小的chunktcache中有5个,而在smallbin中有2个。(一般利用)
  • 至少进行一次calloc
  • 需要UAF来对smallbin中的chunk进行修改

其原理是:

首先需要知道calloccallocmalloc有两个不同点,其一是会自动清零申请到的chunk,其二是calloc不会申请tcache中的chunk。而smallbin有一个特点,那就是当smallbin中的chunk被申请后,其通过bk相连的所有chunk都会被挂入tcache

由此,我们可以修改后插入的chunk(它的bk指针指向main_arena)的bk指针,使其指向我们控制的fake_chunk。只要进行一次callocglibc会使得其从smallbin中申请一个chunk,然后将剩下的smallbin中的chunk都挂入tcache(包括我们修改后的smallbin chunk)。当我们修改后的chunk被挂入tcache后,由于其是通过bk指针来寻找下一个chunk的,因此会将fake_chunk也挂入tcache

然后,其会试图再将fake_chunkbk也挂入tcache,其中会使得:bck->fd=bin,会使得fake_chunkbk指针指向的chunkfd写一个main_arena地址。因此,若我们想要在target_addr写一个main_arena的值,我们需要控制fake_chunkbck的值为target_addr-0x10

tcache_perthread_struct hijacking

上面我们提到了tcache_perthread_struct数据结构的形式为:

1
2
3
4
5
6
7
8
9
/* 每个线程都有一个这个数据结构,所以他才叫"perthread"。  */
// TCACHE_MAX_BINS的大小默认为64
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
// 在glibc2.26-glibc2.29中,counts的大小为1个字节,因此tcache_perthread_struct的大小为1*64 + 8*64 = 0x250(with header)
// 在glibc2.30及以上版本中,counts的大小为2个字节,因此tcache_perthread_struct的大小为2*64 + 8*64 = 0x290(with header)

在程序初始化堆的时候,会创建一个对应大小的tcache_perthread_struct。其中:

  • counts数组存放了每一个大小的chunk目前存放了多少个。
  • entries是一个链表,它里面存放的值是在下一次申请这个大小的tcache chunk的时候,应该分配哪个位置的chunk

要注意,countsentries的对应关系是按顺序来的,例如前0x40个字节中,第0个字节指示大小为0x20tcache chunk的已分配数量,第1个字节指示大小为0x30tcache chunk的已分配数量。又例如,0x40字节处的8字节表示下一个要分配的0x20大小的tcache chunk要分配的地址。

若我们能够控制tcache_pertrhead_struct,则这两个值都可以被篡改。效果分别为:

  • 若我们控制了counts,对指定地方大小的count设置为7,则再次分配该大小的chunk时,就不会分配到tcache中。例如可以分配一个unsorted chunk来泄露libc
  • 若我们控制了entries,相当于实现了任意大小的chunktcache poisoning,即可以在任意地址分配chunk,威力巨大。

tcache浅谈
http://example.com/2023/09/23/system/Heap/tcache/
作者
Ltfall
发布于
2023年9月23日
许可协议