Heap Exploitation - 内存管理概述

本来想全看完源码写的,发现源码有亿点点多。看了后面忘前面,从效率上来说几乎没有效率,只能先了解大概,然后再慢慢看源码。

堆初始化主要是调用了一个核心函数_int_malloc,所以主要是int_malloc函数的相关操作。以下基于glibc2.23,pwn堆基础题也是基于该版本,后续版本的glibc增加了一些结构和检测机制,在堆管理上也有变化。

内存各结构

之前在基础知识已经学习过各个结构,这里再次说明各结构在内存管理中的作用,以便于理解分配与释放过程。

arena

分为main_arena(主分配区)和non_main_arena(非主分配区),主分配区通过系统调用brk()和内存映射系统调用mmap()来分配,非主分配区只能够通过mmap来分配。

chunk

chunk的第二个域的最低一位为P,表示前一个块是否正在使用,为0即表示前一个chunk空闲,这时prev_size才有效,prev_size表示前一个chunk的size,通过这个值可以找到前一个chunk的开始地址。chunk空闲时,M状态不存在,只有AP状态,原本的用户数据区存放了四个指针,fd指向前一个空闲chunk,bk指向后一个空闲chunk。通过这两个指针将相近大小的chunk连成双向链表。large中存在fd_nextsize和bk_nextsize,来加快在large bin中匹配最近的空闲chunk。

bins

释放的内存不是直接归还给操作系统,ptmalloc会统一管理heap和mmap映射区域中的空闲chunk,避免重复的系统调用。相似大小的bin会被用双向链表连接起来,并被称为bin。ptmalloc共维护了128个bin,使用数组来存储。

small bins中的chunk按最近使用顺序进行排列,最后释放的chunk被链接到链表的头部,申请chunk从尾部开始,这样每一个chunk都有相同的机会被选中。

large bins中的chunk按大小排列,相同大小的chunk按最近使用顺序排列。

ptmalloc按”small-first,best-fit“(最小块优先,最佳匹配)的原则来查找空闲chunk。

空闲chunk被链接到bin时,ptmalloc会将该chunk的P位设置为0(这个标志实际上在下一个chunk中)。

fast bins

较小的chunk(小于等于max_fast,64B)的内存空间会被放到fast bin中,并且不会将P位修改,这样就不会将其合并。当用户申请内存时会先检查是否小于等于max_fast,在fast bins中找空闲块。在某个特定的时候ptmalloc会将fast bins中的相邻空闲chunk进行合并,将合并后的chunk加入到unsorted bins中,再加入bins中。

unsorted bins

释放的大小大于max_fast时,或者fast bins中的合并完成后,会被放到unsorted bins中。在进行malloc操作时,如果没有在fast bins找到合适的chunk,就会在unsorted bins中寻找合适的空闲chunk,找不到才会在bins中寻找。如果unsorted bins没有满足大小的chunk,那么就会将unsorted bins中的chunk加入bins中。可以将其理解为一个缓冲区,只是为了加快分配的速度。

top chunk

非主分配区会与预先从mmap分配一块较大的空闲内存模拟sub_heap,通过管理sub_heap来响应需求。内存按地址从低到高分配,在空闲最高处一定会有空闲chunk被称为top chunk。当bins和fast bin都不能满足分配时,ptmalloc就会从top chunk分一块内存给用户。如果不够,就会重新分配一个sub_heap,并将top chunk迁移新的sub_heap,新的sub_heap和已有的sub-heap通过单向链表连接,然后在新的top chunk上分配所需内存。

在glibc内存管理中,“sub-heap” 是指被分配的堆空间被划分为多个更小的块的过程。glibc使用一个名为 “sub-heap” 的机制来管理堆内存,这样可以提高内存分配和释放的效率。

top chunk在fast bins和bins以后才会考虑,top chunk无论多大都不会放到fast bins和bins中。top chunk在分配中大小会不断改变,当回收的chunk的与top chunk相邻时,就会与top chunk合并为新的top chunk。如果free时回收的内存大于某个阈值,并且top chunk的大小也超过收缩阈值,ptmalloc会收缩sub-heap,但是至少要保留一个页大小的空闲内存。如果top chunk包含了整个sub_heap,ptmalloc会调用munmap把整个sub_heap的内存返回操作系统。如果向主分配区的top chunk申请内存,如果top chunk中没有空闲内存,ptmalloc就会屌用sbrk()将进程heap的边界brk上移,然后修改top chunk的大小。

mmaped chunk

所需chunk很大以至于fast bins、bins、top chunk都不能满足要求时,ptmalloc就会使用mmap,来直接使用内存映射将页映射到内存空间,这样分配的内存会在释放时直接还给操作系统,再次引用会造成segmentation fault错误。

last remainder

当需要一个small chunk但是找不到合适的空闲chunk时,如果last remainder大于所需的small chunk,就会将其分割,一个给用户,一个继续作为last remainder。

内存分配概述

  1. 线程先查看线程私有实例中是否已经存在一个分配区,有就会尝试加锁。加锁成功就使用该分配区分配内存,否则就获取一个空闲的分配区,如果所有的分配区都加锁了就会开辟一个新的分配区,并将其加入全局分配循环链表和线程私有实例中,加锁,使用该分配区进行内存分配。开辟出来的新分配区一定为非主分配区,因为主分配区是父进程那里继承来的。开辟非主分配区时会调用 mmap 创建一个 sub_heap ,并设置好 top chunk 。

malloc会遍历所有的可用分配区,遍历的过程中会尝试锁该分配区。当一个分配区对应的线程未使用堆内存则表示可锁。那么如果该分配区可锁,就可以直接被使用。

  1. 将用户申请的大小转换为实际需要分配的大小。

  2. 判断要分配的chunk大小是否小于max_fast,满足就进入下一步,否则转第五步

  3. 试着将fast bins里的第一个chunk取出。如果不为空,那么就会检查该chunk是否出错,没有问题就返回该chunk用户地址并退出,分配结束。

  4. 如果没有在fast bins中找到空闲的或是合适的chunk就会走到这一步,先判断申请的chunk大小是不是小于small bins的最大值(chunk_size < 512B),是就进入下一步,否则进入第七步

  5. 根据需要的chunk大小,从small bins的尾部摘取一个恰好满足大小的chunk,成功就分配结束,否则进入下一步

  6. 到这一步说明需要分配的是一块大内存或者是small bins找不到合适的chunk。ptmalloc会遍历fast bins中的chunk,将相邻的chunk合并,并连接到unsorted bins中。之后会遍历unsorted bins,如果unsorted bins中只有一个chunk,并且在上次分配被使用过,而所需的chunk大小属于small bins,大小也相等,这时就会直接将chunk切割,分配结束,否则就根据大小将其放到small bins或是large bins中。遍历完成,进入下一步

  7. 到这一步说明需要的是一块大内存,或者small bins和unsorted bins中都没有合适的chunk,在上一步中清理干净了fast bins和unsorted bins。那么就从large bins中按照”small-first,best-fit“寻找合适的chunk,从中划分一个所需大小的chunk,将剩下的部分链接回bins中,操作成功分配结束,否则就进入下一步

  8. 如果fast bins和bins都没找到合适的chunk,那么就需要分割top chunk了。如果top chunk满足所需chunk的大小就分割一块,否则进入下一步

  9. 到了这一步,说明top chunk也不能满足需求。这是如果是主分配区,就调用sbrk来增加top chunk大小;如果是非分配区,就调用mmap来分配一个新的sub-heap,增加top chunk的大小,或者直接使用mmap分配,这里需要根据chunk的大小来决定怎么进行分配。如果chunk的大小大于等于mmap的阈值就进入下一步,否则就进入第12步

  10. 使用mmap系统调用为程序的内存空间映射一块chunk_size align 4KB大小的空间,将内存指针返回给用户

    align 4KB:起始地址4KB对齐

  11. 判断是否为第一次调用malloc,主分配区就需要进行初始化操作,分配一块大小为(chunk_size + 128KB)align 4KB大小的空间作为初始的heap;如果已经初始化了,主分配区调用sbrk增加heap空间,非主分配区就在top chunk分割出一个chunk满足分配需求,将内存指针返回

内存回收概述

  1. free也会用到锁,保证线程安全
  2. 判断传入指针

glibc各版本新增机制和修改

glibc2.23

pwn题经典版本,入门题和基础题大多都是glibc2.23版本,堆管理漏洞多并且利用办法多。通常都是基于Dangling pointer和overflow漏洞,和一些不严谨的检测机制,实现任意地址读写。

glibc2.27

添加了tcache结构,tcache bin在glibc2.26引入,用来提升堆管理性能。关于tcache会专门学习,这里只写大概。

  • 添加了TCACHE_MAX_BIN宏定义,定义了管理tcache的最大bin数量,有64个bin被用于管理tcahce bin。对应的chunk size从MALLOC_ALIGNMENT * 2到MALLOC_ALLIGNMENT * 64,64位系统中tcache bin中最大能存储0x800的大小,HACHE_FILL_COUNT定义了bin最多能存储的个数。

  • 添加了两个tcache结构,tcache_entry是一个单向链表,将bin中的chunk连成一个单向链表,机制类似于fast bin。tcache_perthread_struct是用来管理bin的一个结构,其有一个char数组成员,用来记录给个bin已经储存了几个chunk,entries是每个不同大小bins的队头,用来遍历和管理tcache bin。

  • 加入tcache和取出tcache的两个函数。

malloc和free的逻辑也有一些改变

  • _libc_malloc中,在调用int_malloc函数之前会先去搜索tcache是否有空闲的chunk,有就直接从tcache分配,没有才执行int_malloc
  • _int_malloc中,fast bins和small bins和unsorted bins中多余的块会被放入tcache中
  • free会先检查是否处于tcache范围,tcahce未满就会直接put进去

glibc2.29

修改了一些malloc和free chunk的检测机制。

在tcache新增了检测字段key,主要是用来检测double free

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

在加入tchche bins时会将key字段赋值为tchche的地址,取出时会将其清零。

int_free新增检测机制

新增了在tcache释放时对key字段的检验,如果当前释放的chunk的key字段位为tcache时,判断其为double free

unlink新增检测机制

unlink解链前,会检查chunk大小是否与presize相同,增加了null of byte的难度

int-malloc新增检测机制

新增关于unsorted bins的检测,检测chunksize的合法性,next chunk size的合法性,以及双向链表的完整,unsorted bin attack攻击很难继续使用

use_top新增检测机制

在_int_malloc中对use_top新增了检测机制,使用top chunk时对top chunk的大小进行了检测,使得horse of force无法再使用

if (__glibc_unlikely (size > av->system_mem))//0x21000
malloc_printerr ("malloc(): corrupted top size");

glibc2.30

在_int_malloc中,将unsorted bins中的内容放入large bins时会检测fwd链表的完整性,使得large bin attack无法再使用

glibc2.31

暂无

参考