堆利用是基础linux用户态pwn门槛比较高的一个点,主要由于需要审计glibc源码劝退了大部分人。其实glibc对ptmalloc2的实现,本质上就是做链表维护。理解了ptmalloc2中链表维护的思想,那么相应的利用手法也很容易掌握。
前置知识
只有链表,不了解的可以自行补习数据结构。
chunk(堆块)
本节我将以64位为例讲解malloc_chunk(堆块)结构体
初识
可以在gdb中用vis指令查看chunk(vis n可以查看n个chunk)
图中每个颜色代表一个chunk,我们留意紫色的部分:

chunk有一个必不可少的属性——大小,就是图中0x0000000000000071的8个字节,我们记为size。size记录了两个重要的信息:
- 二进制低3位代表当前chunk的状态,我们约定是为1否为0,最低位表示前一个chunk是否仍在使用,次低位表示该chunk是否是通过mmap申请的,最高位表示改chunk是否属于子线程。其中最低为比较重要,后续的利用手法会用到。
- 剩余部分表示chunk的大小,比如此chunk的大小为
0x70。chunk的大小只能为二进制低4位为0000的整数,即必须为16的倍数,而且必须大于0x20。
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 | typedef struct tcache_entry |
需要注意的是,tcache中next保存的是 “可使用”部分的起始地址,并不是chunk的起始地址。
fastbins和tcache还有一个共同点,就是即使chunk被释放,chunk的size低3位是不会被更新的,这一点要注意。
prev_size & fd & bk & unsorted bin
prev_size
我们再来看这张图中紫色的chunk:

prev_size是size之前的8个字节,比较特殊的是,当前一个chunk(蓝色)正在被使用时,这8个字节是属于前一个chunk的“可使用”部分。同理,位于0x25050e0的8个字节(紫色)也应该是下一个chunk(绿色)的prev_size。当前一个chunk被释放时,而且如果没有被放入fastbins/tcache,例如unsorted bin,那么这个紫色的chunk也会被更新:
prev_size原来属于前一个chunk(蓝色)的“可使用”部分,而这一部分随着chunk被释放而不再可以被用户使用,由ptmalloc2接管。prev_size的位置会被写入前一个chunk(蓝色)的大小。size字段的最低位(二进制位)也会被修改,由于前一个chunk不再处于“被使用”的状态,size字段的最低位会被修改为0。
进行这样的操作目的只有一个:当这个紫色的chunk被释放时,ptmalloc2可以很方便地得知这个chunk和前一个chunk(蓝色)是可以进行合并的,进而更高效地管理堆内存。
fd & bk
fd和bk类似于next,同样只在被释放的chunk中才会启用,比较特殊的是,只有在被释放的chunk不能放入tcache和fastbins时,fd和bk才会启用。
fd对应size后面8个字节,bk对应fd后面8个字节。fd类似于双链表中节点的prev,用来储存前一个空闲的chunk的起始地址;bk则类似于双链表中的next,储存后一个chunk的起始地址。
这里我们借助unsorted bin理解fd和bk。
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时,其fd和bk都是main_arena.top的地址。(仅适用于主线程中的情况,初学者可以忽略这一点)
这里简单介绍一下main_arena,main_arena是主线程用于储存heap中相关数据结构的结构体,各种bin链表的头部(比如fastbinsY数组)、一次申请堆空间的大小还有fastbinsY的长度等都记录在这里。但是main_arena不直接参与tcache的管理。
与unsorted bin类似的还有smallbins,这里只是简单展示一下smallbins的双链表结构:


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

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

可以看到,正如上文所述,fd_nextsize和bk_nextsize紧跟在bk后面。类似于unsorted bin,largebins头节点的fd和尾节点的bk也是存着main_arena.bins[x]的地址,其中x取决于largebin中存放chunk大小的范围。
根据图中的信息,我们可以构建这样的关系:

这个fd bk组成的双链表(黑色箭头)我们已经在unsorted bin中见过了。而fd_nextsize和bk_nextsize则构成了图中带颜色箭头组成的结构。不难看出,fd_nextsize和bk_nextsize是把不同大小的chunk看成一组,再从每一组中选取第一个进入largebin的chunk,将这些chunk串成了一个循环链表,其余chunk的fd_nextsize和bk_nextsize都是0。
至此关于chunk内容已经介绍完毕,接下来介绍各种bin以及管理机制