关于如何理解Glibc堆管理器(Ⅰ——堆结构)

First Post:

Last Update:

 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源。

首先从 什么是堆 开始讲起吧。

        在操作系统加载一个应用程序的时候,会为程序创建一个独立的进程,这个进程拥有着一套独立的内存结构。大致结构如下图:

         进程在运行之处会创建一块固定大小的堆空间,但当用户需要申请一块超出已有堆空间大小的内存时,操作系统就会调用**sbrk函数(也有其他类似功能的函数)**来延申这块空间

        但正如我们所见,这样的拓展大小的方式似乎还是有极限的。当即便用sbrk去拓展Heap,也不能够满足用户的需求的时候(至少堆不能覆盖到栈上去,对吧?),操作系统就会使用mmap来为进程开辟额外的空间,这些空间可以被视为“虚拟内存”,它们不需要时刻都加载在内存中,因此能够大大提升堆的空间

        当然,如果即便如此也不能够满足用户所需要的空间,那这个申请空间的操作就会失败,例如malloc,它会返回一个NULL

        并且,上图还显示了堆在内存中的结构——一段连续的内存块,记住这个特点将对接下来的理解很有帮助

如下为一个堆的结构体申明:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct malloc_chunk {

INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free).*/
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;

};

        这可能会给人一种反直觉的印象,因为这个堆结构体似乎太小了,根本不能够像我们印象里的那样去存放数据

        因此这里需要介绍一下Glibc中堆的寻址方式——隐式链表

         尽管上图已经很详尽的介绍了堆的存放,但我仍然有必要多做些说明

        操作系统会将堆划分成多个chunk以分配给程序,也就是malloc请求到的实则是一个chunk

        而malloc返回的指针实则是指向**chunk+16(在x86中则是+8)**处的地址,究其原因就是因为结构体中的如下两项

1
2
INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).*/
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead.*/

        只有这两个数据是常驻于结构体中的(这句话有些晦涩,现在看不懂也没关系)

        它们分别表示上一个chunk的大小当前chunk的大小,那既然我们能够知道上一个chunk的大小,通过简单的加法就能够找到上一个chunk的位置了,这种方法就被称为隐式链表

        而在mchunk_size的下面就是用来储存用户需要的数据

        显然,如果从这个地方开始储存数据,上面给出的结构体就会被破坏了,因为另外四个成员无处安放了,但对于一个正被使用的chunk来说,这是无关紧要的,因此才说它们并不常驻(其中原因牵涉了其他,也将在下文叙述)

        (但请注意,chunk块的申请是要符合**16字节(或8字节)**对齐的,尽管用户申请的时候看起来相当随意,但操作系统仍然会返回对齐后的堆结构)

        同时,为了节省资源,mchunk_size的最后三位将用来储存额外的标志位,其意义这里不再赘述,但这里需要再一次强调的是,最后一位 P标记位 指示了上一个chunk是否处于被使用状态

尽管它们被用作标记,但在计算chunk大小的时候,我们会默认它们为0以计算合理大小

例如(二进制)1000101:Size=1000000,A=1,M=0,P=1

实际操作:

示范程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main()
{
unsigned long long *chunk1, *chunk2;
chunk1=(unsigned long long)malloc(0x80);
chunk2=(unsigned long long)malloc(0x80);
printf("Chunk1:%p",chunk1);
printf("Chunk2:%p",chunk2);
return 0;
}

         通过如下命令去编译这个文件

1
gcc -g heap.c -o heap

        然后用gdb调试heap文件,我们将断点定在第11行,查看此时的堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gdb-peda$ heap
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602090 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602120 PREV_INUSE {
prev_size = 0x0,
size = 0x20ee1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

        可以看出,我们申请的chunk大小为0x80,但实际返回的chunk却有0x90(最后的1为标志位)

        同时,它们是严格的按照堆的顺序往下开辟的,从0x602000到0x602090,没有其他空挡

        而0x602120是则是被称为“Top chunk”的堆结构,在当前的堆仍然充足的时候,操作系统通过分割Top Chunk来提供malloc的服务

1
2
3
4
gdb-peda$ p chunk1
$1 = (unsigned long long *) 0x602010
gdb-peda$ p chunk2
$2 = (unsigned long long *) 0x6020a0

        而查看chunk1的内容,发现它指向0x602010而不是0x602000

        这也作证了前面所说的内容,在这空挡的16字节中储存了常驻的那两个成员,而其他成员则被舍弃了

图片来源:https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/

插画ID:72077484