Heap vulnerabilities
前置知识:malloc/free
- 堆通过malloc/free来管理空间。可能存在如下安全漏洞:
- 释放后使用(UAF)
- 双重释放
- 差一错误
堆的使用规范
- 当malloc的指针传递给free后禁止再对该指针进行读写操作
- 不要在堆分配中使用或泄漏未初始化的信息
- 不要读取或写入超过堆分配结束的字节
- 不要重复传递从malloc到free的指针
- 在分配开始前不要读取或写入字节
- 不要传递不是由malloc初始化的指针给free
- 在检查函数是否返回NULL前不要引用malloc指针
堆的内存分配
内存分配方式
- 堆内存是通过从内核调用sbrk系统来分配的
- 使用mmap来处理大内存分配,这是堆外分配,不在下面的讨论之内
堆相关微观结构
malloc_chunk
- 我们称malloc申请的内存为
chunk,在ptmalloc内部用malloc_chunk结构体表示,定义如下:
|
|
- 字段解释:
- prev_size: 如果该
chunk的物理相邻的前一个chunk是空闲的,在这里记录前一个chunk的大小;否则这里记录的是前一个chunk的数据。 - size: 该
chunk的大小,该大小必须是MALLOC_ ALIGNMENT的整数倍。如果不是,那么会被转换为满足大小的最小的MALLOC_ ALIGNMENT的整数倍,这通过request2size()宏完成。另外该字段的低三位对不记录大小,它们从高到低分别表示:- NON_MAIN_ARENA: 记录当前
chunk是否不属于主线程,1表示不属于,0表示属于 - IS_MAPPED: 记录当前
chunk是否是被mmap分配的 - PREV_INUSE: 记录前一个
chunk块是否被分配。一般来说,队中的第一个被分配的chunk块的size字段的P位都会设置为1,以防止访问前面的非法内存。当一个chunk的size地段的P位为0时,可以通过prev_size字段来获取上一个chunk的大小以及内存地址
- NON_MAIN_ARENA: 记录当前
- fd,bk:
chunk处于分配状态时,从fd字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下- fd指向下一个(非物理相邻)空闲的
chunk - bk指向上一个(非物理相邻)空闲的’chunk`
- fd指向下一个(非物理相邻)空闲的
- fd_nextsize, bk_nextsize: 也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
- fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。
chunk结构- 栈示意图:
* 一个已经分配的 chunk 的样子如上。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。 当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。 * 被释放的`chunk`被记录在链表中(可能是循环双向链表,也可能是单向链表)。可以发现如果一个`chunk`处于free状态,会有两个位置记录其相应的大小。即本身的size字段和后面的`chunck`。一般情况下,物理相邻的两个空闲`chunck`会被合并为一个。堆管理器会通过 prev_size 字段以及 size 字段合并两个物理相邻的空闲`chunk`块
- prev_size: 如果该
bin
bin的定义
- 堆管理器需要跟踪释放的块,以便malloc可以在分配请求期间重用它们
- 堆管理器维护一系列被称为"bin"的列表来最大限度地提高分配和释放的速度
bin的分类
- 共有5种容器:62个小容器,63个大容器,1个未排序容器,10个高速缓存容器,以及每个线程独有的64个线程缓存容器(如果启用)
- 小容器、大容器以及未排序容易被用于实现堆的基本回收策略,高速缓存容器和线程缓存容器则是实现优化
small bin
large bin
unsorted bin
fast bin
tchache bin
堆相关宏观结构
Arena
- 每个arena就是一个独立的堆,独立地管理chunk和bin
- 对于每个新加入的线程,会试图找到一个没有其他线程正在使用的arena,并且将该arena附加到该线程上。
- 如果所有arena都被现有的线程使用,那么会创建一个新的arena,注意arena数量存在上限,对于32位架构为
2*CPU内核数,对于64位架构为8*CPU内核数。 - 如果arena数量达到上限,将会出现线程共用aren以及随之而来的线程等待的可能。
堆上漏洞
UAF(USE-AFTER-FREE)
- 错误:在释放了堆上的内存后引用(又名悬垂指针引用)
- 后果:攻击者可以使用被释放的指针控制数据写入
- 错误示例:
在该例子中,当buf1被释放后,该内存就立刻可以重用,之后在为buf2和buf3分配空间时可能分配了该内存。使用被释放的指针进行写操作就可能会覆盖buf2和buf3
1 2 3 4 5 6 7 8 9 10 11 12int main(int argc, char** argv) { char *buf1, *buf2, *buf3; buf1 = (char*)malloc(BUFSIZE1); free(buf1); buf2 = (char*)malloc(BUFSIZE2); buf3 = (char*)malloc(BUFSIZE3); strncpy(buf1, argv[1], BUFSIZE-1); } - 利用UAF: 覆盖控制流数据
- 预防UAF: 将被释放的指针设置为NULL
双重释放(Double Free)
- 示例:
|
|
- 代码工作:
- 释放buf1,然后分配buf2
- buf2可能占用buf1相同的内存空间
- buf2获取用户提供的数据
- 再次释放buf1
- 其中可能使用一些buf2数据作为元数据
- 并且可能打乱buf2的元数据
- 然后是buf2,此时使用了混乱的元数据
- 释放buf1,然后分配buf2
- 双重释放可以达到与堆溢出漏洞类似的效果,可以使用类似的方式预防
空字节溢出(Off- by-Null)
- 堆溢出一字节: 将缓冲区改为0
- 利用方式: 将P从1改写为0
- 这将导致前一个块被视为空闲
- 下一个块的释放会将空闲块合并
- 攻击流程:
- 分配内存,定位地址
- 空字节溢出
- 断链
- 写入覆盖