glibc堆管理器简介

JnamerZ 2024-07-11 19:49:46
Categories: Tags:

堆利用是基础linux用户态pwn门槛比较高的一个点,主要由于需要审计glibc源码劝退了大部分人。其实glibc对ptmalloc2的实现,本质上就是做链表维护。理解了ptmalloc2中链表维护的思想,那么相应的利用手法也很容易掌握。

前置知识

只有链表,不了解的可以自行补习数据结构。

chunk(堆块)

本节我将以64位为例讲解malloc_chunk(堆块)结构体

初识

可以在gdb中用vis指令查看chunk(vis n可以查看n个chunk)

图中每个颜色代表一个chunk,我们留意紫色的部分:

chunk有一个必不可少的属性——大小,就是图中0x0000000000000071的8个字节,我们记为sizesize记录了两个重要的信息:

size后面紫色的部分,是这个chunk的“可使用”部分,大小为size - 8。同时要注意的是,chunk的起始地址实际上为size地址减8 ,即图中的0x2505070,前八个字节对应为prev_size,后文介绍unsorted bin时会再作详细介绍。

当然,这个部分“可使用”的前提是这个chunk没有被释放(free)。释放时,“可使用”部分的前几个字节会具有特殊的含义,下文会作详细解释。free函数的参数即为“可使用”部分的地址,即图中的0x2505080

next & fastbins & tcache

低版本glibc

当满足条件时,被释放的chunk会被放入fastbins。这里fastbins指的是glibc中的fastbinsY数组(位于libc段),数组中的每个元素代表一个链表的头部,链表中保存着大小相同的chunk,每个chunk也同时为链表的一个节点,如下图所示:

fastbinsY中保存着大小为0x20、0x30、0x40……的chunk的链表,next保存着下一个chunk的起始地址。当一个chunk被释放时,ptmalloc2会判断这个chunk是否要放入fastbins。如果满足条件,那么chunk就会被放入fastbinsY中对应链表的头部,比如上一小节的紫色chunk,就会被放入储存在fastbinsY[5]的链表头部。

在gdb中,用bins指令可以看到fastbins中储存的空闲chunk:

fastbins的管理策略为LIFO,即后入先出。比如上图中fastbins中已有3个chunk,若此时释放一个大小为0x70的chunk,将会放到chunk的头部;同样,如果调用malloc(0x70);那么就会从fastbinsY[5]的头部取出一个chunk,返回给程序使用。

对于fastbins的检查很少。比较常见的是在free时,会检查将要被free的chunk的起始地址,如果发现chunk在对应大小链表的头部,那么则会报错退出。

double free or corruption (fasttop)

高版本glibc

glibc2.26后加入了tcache管理机制,相对于fastbins更快速,检查更少,也更容易被利用。(fastbins没有被删除)

tcache的本质和fastbins类似,但是tcache会保存各个链表的长度,这些信息保存在heap段的第一个chunk,大小一般为0x290,详细可以参考结构体源码:

1
2
3
4
5
6
7
8
9
10
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry; //chunk在tcache中的结构体类型
//可以看到,chunk在tcache中只是一个单链表的节点,而且没有size信息
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct; //保存tcache链表信息的结构体

需要注意的是,tcache中next保存的是 “可使用”部分的起始地址,并不是chunk的起始地址。

fastbins和tcache还有一个共同点,就是即使chunk被释放,chunk的size低3位是不会被更新的,这一点要注意。

prev_size & fd & bk & unsorted bin

prev_size

我们再来看这张图中紫色的chunk:

prev_sizesize之前的8个字节,比较特殊的是,当前一个chunk(蓝色)正在被使用时,这8个字节是属于前一个chunk的“可使用”部分。同理,位于0x25050e0的8个字节(紫色)也应该是下一个chunk(绿色)的prev_size。当前一个chunk被释放时,而且如果没有被放入fastbins/tcache,例如unsorted bin,那么这个紫色的chunk也会被更新:

进行这样的操作目的只有一个:当这个紫色的chunk被释放时,ptmalloc2可以很方便地得知这个chunk和前一个chunk(蓝色)是可以进行合并的,进而更高效地管理堆内存。

fd & bk

fdbk类似于next,同样只在被释放的chunk中才会启用,比较特殊的是,只有在被释放的chunk不能放入tcache和fastbins时,fdbk才会启用。

fd对应size后面8个字节,bk对应fd后面8个字节。fd类似于双链表中节点的prev,用来储存前一个空闲的chunk的起始地址bk则类似于双链表中的next,储存后一个chunk的起始地址

这里我们借助unsorted bin理解fdbk

unsorted bin

当被释放的chunk不满足条件,不能被放入fastbins/tcache时,会被放入unsorted bin中。可以用bins查看当前unsorted bin中储存的chunk(倒序):

unsorted bin的管理策略是FIFO,即每次从unsorted bin的头部插入chunk,从尾部取出chunk。从unsorted bin取出chunk可以被分割出一部分,以供malloc返回给程序使用,因此取chunk的条件比较宽松。

输入heap指令可以看到堆中所有的chunk的状态,这里我们可以看到位于堆中三个位于unsorted bin中的chunk(绿色)。同时,可以注意到,被夹在他们中间的chunk(橙色)的size的最低位被设置为了0,表示其前一个chunk处于空闲状态。

结合上两个图可以明显看出,unsorted bin是一个双链表,其头部保存在libc段。比较特殊的是,双链表头部节点的fd和尾部节点的bk存着main_arena.top的地址。那么易知,当unsorted bin中只有一个chunk时,其fdbk都是main_arena.top的地址。(仅适用于主线程中的情况,初学者可以忽略这一点)

这里简单介绍一下main_arenamain_arena是主线程用于储存heap中相关数据结构的结构体,各种bin链表的头部(比如fastbinsY数组)、一次申请堆空间的大小还有fastbinsY的长度等都记录在这里。但是main_arena不直接参与tcache的管理

与unsorted bin类似的还有smallbins,这里只是简单展示一下smallbins的双链表结构:

fd_nextsize & bk_nextsize & largebins

fdbk后面16字节依次是8字节的fd_nextsize和8字节的bk_nextsize,这两个部分仅在chunk被置入largebins中启用,因此我们可以借助largebins来理解fd_nextsizebk_nextsize

我们来分析下面这个largebin:

ps: 图中的chunk是按地址大小的顺序,从小到大被置入largebin的

可以看到,正如上文所述,fd_nextsizebk_nextsize紧跟在bk后面。类似于unsorted bin,largebins头节点的fd和尾节点的bk也是存着main_arena.bins[x]的地址,其中x取决于largebin中存放chunk大小的范围。

根据图中的信息,我们可以构建这样的关系:

这个fd bk组成的双链表(黑色箭头)我们已经在unsorted bin中见过了。而fd_nextsizebk_nextsize则构成了图中带颜色箭头组成的结构。不难看出,fd_nextsizebk_nextsize是把不同大小的chunk看成一组,再从每一组中选取第一个进入largebin的chunk,将这些chunk串成了一个循环链表,其余chunk的fd_nextsizebk_nextsize都是0。

至此关于chunk内容已经介绍完毕,接下来介绍各种bin以及管理机制