glibc 2.35 malloc/free全流程

2025-06-04

PS:之前的利用基本都集中在tcache中,但是libc的堆管理远不止tcache,现在我们来看看libc中堆管理到底会怎么分配内存

0. 回顾: tcache

到目前为止,我们学习过的tcache,有四个明显的特性:

1. Bins of constant size up to 1032 bytes
2. Caches up to seven freed chunks
3. Singly linked list
4. Safe-Linking

tcache被free时,其元数据如下所示:
其中size的前3位,分别为A(标志chunk是否是main_arena以外的arena分配),M(标志chunk是否是mmap出来的),P(prev_inuse位,标志前一个chunk是否被使用,1都是已经被使用)
下图为一个tcachebin的结构:

1. glibc 2.35: malloc/free

1.1 free

其实可以分析一下具体流程:

1. 如果chunk能放入tcachebin,就会放入tcachebin中;如果不能放入tcachebin,进入第二步
2. 如果chunk能放入fastbin,就会放入fastbin中;如果不能放入fastbin,进入第三步
3. 判断该chunk是不是mmap出来的,如果是,调用munmap进行回收;如果不是,进入第四步
4. 该chunk是不是大于65kb,如果是,clear并consolidate fastbin中的chunk,并进入第五步;如果不是,直接进入第五步
5. 尝试合并该chunk前后的chunk。若该chunk能和top chunk合并,直接合并;否则,将该chunk放入unsortedbin中。

第四步提到的clear并consolidate fastbin中的chunk,这个步骤其实相当重要,我们之后会再提到

1.2 malloc

1. 从tcache中找符号大小的chunk,如果找到,从tcache中取出,并返回;如果没有符号的chunk,进入第二步
2. 从fastbin中找符合大小的chunk,如果找到,从fastbin中取出,并返回;如果找不到,进入第三步
3. 从smallbin中找符合大小的chunk,如果找到,从smallbin中取出,并返回;如果找不到,进入第四步
4. consollidate fastbins(具体做的就是fastbin解链,设置下一个chunk的pre_size域和pre_inuse域,尝试合并chunk,并放入到unsortedbin中),从unsortedbin中找符合大小的chunk,如果找到,从unsortedbin中取出,并返回;如果找不到,进入第五步。注意在遍历unsortedbin的过程中如果trunk不符合大小,在小时,我们会把它归类到smallbin/largebin中,在大时,我们会对其切割,切割部分返回,剩余部分仍然在unsortedbin中
5. 从largebin中找符合大小的chunk,如果找到,从largebin中取出,并返回;如果找不到,进入第六步。
6. 判断申请的大小,如果很大,使用mmap申请内存并返回;如果不是,进入第七步。
7. 从wilderness中切割chunk并返回。

1.3 tcachebin之外的chunk metadata

1.4 double list

fastbin和tcachebin都是单向链表,然而,smallbin、unsortedbin、largebin是双向链表,其结构大致可以如下所示:
为什么要整一个双向链表呢?因为双向链表在拆除其中一个chunk的时候非常方便。
对于单向链表而言,除非是摘靠前的trunk,那么摘是比较容易的,但是如果摘得trunk比较靠后,那就很麻烦了,需要大量的性能开销.
双向链表就不会,双向链表中的trunk在做摘除时,不需要遍历整个链表。
glibc中涉及这种频繁的摘除操作的步骤叫做consolidation(已经提到过了)

1.5 consolidation

consolidation就是双向链表的原因了。它的主要功能是将相邻的两个chunk合并为一个(并不是链表中,合适物理上相邻的两个chunk),在free/malloc中都会发生(正如我们上面提到过的)。问题在于,一旦发生了合并,被合并的chunk就需要从它所在的链表中摘除出来(叫做unlinking)。
consolidation既可以前向合并,也可以后向合并。
跟踪相邻的chunk主要依靠prev_size域,来计算上一个chunk的地址。判断相邻的chunk可不可以合并,依靠的是prev_inuse位。

这种consolidation一般发生在释放大于65kb的chunk时,和malloc大于1024字节的chunk时

1.6 unlinking

unlink的部分代码如下:

static void unlink_chunk (mstate av, mchunkptr p) {
 if (chunksize (p) != prev_size (next_chunk (p)))//检查一
    malloc_printerr ("corrupted size vs. prev_size");
  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;
  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))//检查二
    malloc_printerr ("corrupted double-linked list");
  fd->bk = bk;
  bk->fd = fd;

2. glibc中各种的bins

2.1 tcache

可以查看https://wsxk.github.io/heap/中的tcache部分

2.2 fastbin

1. Singly linked list with safe-linking - similar to tcache(有safe-linking机制的单向链表)
2. Bin lists grow to unlimited length(链表长度没有限制,tcache是7个)
3. Bins of constant size up to 0x80 bytes(最大的大小是0x80)
4. P bit is never cleared for chunks in the fast bin(跟tcache一样,prev_inuse不会被清0)
5. Only checks top chunk for double-free(检查较少,很容易double free)

它的结构是长这样的:

2.3 smallbin

1. Doubly linked lists(双向链表,因为会有unlink操作,能够提高速度)
2. Bins of constant size up to 1024 bytes(最大到1024字节)
3. Fast access, but capable of consolidating(能够进行consolidate)

2.4 unsortedbin

1. Doubly linked list(双向链表)
2. Holds large and small bin values (anything that cannot go in fast bins)(free)

On Malloc:
3. Unsorted bin chunks are checked 
4. If chunk does not satisfy malloc, it is placed in appropriate small/large bin

2.5 largebin

1. Doubly linked lists(双向链表)
2. Bins consist of a range of sizes(一个bin存放的是一个范围chunk,这给维护带来了额外挑战)
3. Chunks in each bin are sorted by size,  largest first(最大size的chunk被放在链首,且fd/bk指向的相同大小的chunk,然而!!!fd_nextsize/bk_nextsize指向其他size的chunk)
4. bk_nextsize is used “jump up” in size category quickly

2.6 wilderness和mmap

从topchunk切割出chunk,如果切割不出来,就会用mmap申请一块内存。

3. glibc 利用

在知道了glibc的全貌的情况下,能够利用的方法就多了很多(包括但不限于tcache)接下来我们再介绍几个有趣的利用手法(不过事先强调,glibc的利用很依赖版本,因为不同版本有不同的安全校验)
为了能够更好的演示,可以写个demo调一下:

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(){
    void * alloc_slots[16];
    char command[128];
    int size;
    int index;
    while (1){
        printf("malloc/free \n");
        scanf("%s",command);

        if(strstr(command,"malloc")){
            scanf("%d",&index);
            scanf("%d",&size);
            alloc_slots[index] = malloc(size);
            printf("alloc_slot[%d] = malloc(%d) = %p\n",index,size,alloc_slots[index]);
        }else if(strstr(command,"free")){
            scanf("%d",&index);
            free(alloc_slots[index]);
            printf("free(%p)\n",alloc_slots[index]);
        }
    }
}

注意以下技巧或多或少都借用了之前提到的uaf,double free,Corrupting Heap Metadata,Overlapping Allocations等漏洞

3.1 Taking Advantage of Consolidation

Recall: chunks go to the unsorted bin 

1. A correctly sized (large) malloc can clear fastbin and cause consolidation(比如0x400)
2. Conversely, sometimes you will hold an allocation solely to prevent consolidation

3.2 Moving chunks between bins

1. Chunks move from the unsorted bin to the small/large bins
  after they have been rejected during a malloc call

2. Clear fastbin with large malloc call

3. Small chunks can move into tcache from smallbin opportunistically(如果tcachebin中相同大小的bin是空的且申请了smallbin中的同size的chunk)

3.3 Creating Fake Chunks

能够控制某块内存区域的话,可以精心构造数据让glibc误识别

3.4 Unsafe Linking / Unlinking

利用unlinking机制完成解链。