一. 堆结构的介绍
堆是一种动态分配的内存。在用户态由malloc() API调用申请,而malloc()内部会通过调用brk或mmap系统调用来实现。(此部分参考 syscalls used by malloc)
进程的虚拟内存空间布局如下图所示:
其中:start_brk是堆的起始地址、brk是堆的结束地址。
brk系统调用:通过增加brk的值,实现从内核中获取内存 —— 用于申请<=128kb的内存,不会被初始化为0,分配的内存在堆区域。
当进程一开始未分配任何堆内存时,start_brk=brk:
- 当ASLR未打开时,start_brk和brk指向data/bss段的结束位置(即end_data)
- 当ASLR打开时,start_brk和brk指向end_data加上一个随机偏移(random brk offset)的位置
mmap系统调用:创建一个私有的匿名映射段(private anonymous mapping segment)供当前进程使用 —— 用于申请>128kb的内存,会被初始化为0,分配的内存在堆和栈的中间区域。 其中,private表示该段仅属于当前进程所有,anonymous表示该段并不映射到任何磁盘文件。
对于堆内存管理而言,最小操作单位即为chunk。(此部分参考understanding libc malloc)
主要有2种类型的chunk:allocated chunk 和 free chunk。chunk的结构如下图所示:
1个chunk主要由2大部分组成,分别是header 和 mem。
header包含2个字段:
- prev_size:如果前一个块已经被释放了,则存储前一个块的size(用于释放当前块时寻找到前一个块,并与其合并后共同释放);否则存储前一个块的用户数据(因此malloc实际分配的mem大小会由申请的大小-4再根据8字节对齐来确定,比如当malloc(10)时,实际的mem部分只占了8个字节,剩余2个字节会写入下一个块的prev_size字段,同时计算当前块的size时不会将这2个字节所占空间考虑进去)
- size:当前chunk的整体大小,包括header和mem;由于堆分配时要求8字节对齐,因此size肯定是8的倍数,那么其最后3位肯定为0,可用于存储其他信息,如标志位:
- PREV_INUSE(P) 表示前一个块是否被分配了(在free时会用到)
- IS_MMAPPED(M) 表示当前块是否被mmap
- NON_MAIN_ARENA(N) 表示当前块是否属于一个thread arena,而非main arena
mem即为malloc分配后返回给用户的指针所指向的内存:
- 在allocated chunk中从此地址开始为用户可使用堆空间
- 在free chunk中此部分存放两个指针fd和bk,用于将被释放的chunk链接在一起,以便下次malloc时更快地从已释放内存中索引到大小合适的堆块来分配。
二. 堆相关漏洞
1. heap overflow
当分配多个堆时,往一个堆的mem区域strcpy数据,有可能会超过当前chunk的size,从而覆盖了后一个堆的header或mem部分,进一步造成对后一个堆执行一些操作时实现了攻击者的目的。
unlink造成的"write anything to anywhere"就是通过heap overflow实现的,下面详细讲解unlink。(此部分参考heap overflow using unlink)
unlink应用场景:当释放一个堆时,如果这个堆块不是mmaped的话,则会去考虑合并前后释放块,即判断当前块的前一块或后一块是否已经被释放,如果是的话则将其从其所在的bin中取出来(即unlink),然后与当前要释放的块进行合并;最后再将合并后的块放入大小适当的bin中去。
说明:bin是freelist数据结构,用于存储释放后的块,根据块大小不同可分为fast bin, unsorted bin, small bin, large bin (此处不做详细说明,感兴趣的可看understanding libc malloc)
细节说明:
(1) 如何检查当前块的前一块或后一块是否被释放 - check free
前一块:查看当前块的size字段中的PREV_INUSE标志位是否为0,若为0则表示前一块已经被释放;然后根据prev_size字段来找到前一块的地址
后一块:查看当前块的后一块的后一块的PREV_INUSE标志位是否为0(即表示当前块的后一块是否被释放);而后一块的地址是通过size字段来获取到的,这里next-to-next块即为 “当前块的指针+当前块size+下一块的size”
(2) 如何unlink
即:[P->fd + 12] = P->bk
[P->bk + 8] = P->fd
(3) 如何合并
将unlink块的size添加到当前块的size,根据添加后的size选择要加入的bin
2. use after free
应用场景:当某个块被释放后,由于释放仅仅是将其加入bin中,在内存中该块内容仍然存在;若此时再次分配一块与释放块大小一样的内存时,则会将该free块再次分配,也就是说当前块与free块其实指向同一块空间;若此时再去访问被释放的指针时程序并不会报错,而可直接访问那块空间。因此通过在第2次malloc后对这块空间的操作,可实现再次访问free掉的变量时,按照该变量的方式来解释该空间中的数值,从而实现一些程序逻辑方面上的功能。
ps:最后推荐一个适合初学者学习堆栈漏洞的视频 LiveOverflow Binary Hacking