本文主要是介绍关于如何理解Glibc堆管理器(Ⅰ——堆结构),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。
若有图片及文稿引用,将在本篇结尾处著名来源。
目录
什么是堆
实际操作
首先从 什么是堆 开始讲起吧。
在操作系统加载一个应用程序的时候,会为程序创建一个独立的进程,这个进程拥有着一套独立的内存结构。大致结构如下图:
进程在运行之处会创建一块固定大小的堆空间,但当用户需要申请一块超出已有堆空间大小的内存时,操作系统就会调用sbrk函数(也有其他类似功能的函数)来延申这块空间
但正如我们所见,这样的拓展大小的方式似乎还是有极限的。当即便用sbrk去拓展Heap,也不能够满足用户的需求的时候(至少堆不能覆盖到栈上去,对吧?),操作系统就会使用mmap来为进程开辟额外的空间,这些空间可以被视为“虚拟内存”,它们不需要时刻都加载在内存中,因此能够大大提升堆的空间
当然,如果即便如此也不能够满足用户所需要的空间,那这个申请空间的操作就会失败,例如malloc,它会返回一个NULL
并且,上图还显示了堆在内存中的结构——一段连续的内存块,记住这个特点将对接下来的理解很有帮助
如下为一个堆的结构体申明:
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)处的地址,究其原因就是因为结构体中的如下两项
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
实际操作:
示范程序:
#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;
}
通过如下命令去编译这个文件
gcc -g heap.c -o heap
然后用gdb调试heap文件,我们将断点定在第11行,查看此时的堆
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的服务
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/
这篇关于关于如何理解Glibc堆管理器(Ⅰ——堆结构)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!