堆漏洞

讨论堆的架构与漏洞的利用

Heap vulnerabilities

前置知识:malloc/free

  • 堆通过malloc/free来管理空间。可能存在如下安全漏洞:
    • 释放后使用(UAF)
    • 双重释放
    • 差一错误

堆的使用规范

  • 当malloc的指针传递给free后禁止再对该指针进行读写操作
  • 不要在堆分配中使用或泄漏未初始化的信息
  • 不要读取或写入超过堆分配结束的字节
  • 不要重复传递从malloc到free的指针
  • 在分配开始前不要读取或写入字节
  • 不要传递不是由malloc初始化的指针给free
  • 在检查函数是否返回NULL前不要引用malloc指针

堆的内存分配

内存分配方式

  • 堆内存是通过从内核调用sbrk系统来分配的
  • 使用mmap来处理大内存分配,这是堆外分配,不在下面的讨论之内

堆相关微观结构

malloc_chunk

  • 我们称malloc申请的内存为chunk,在ptmalloc内部用malloc_chunk结构体表示,定义如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct malloc_chunk
{
  INTERNAL_SIZE_T prev_size; /* 前一个相邻块的大小(仅当它空闲时有效)。否则被当前块的用户数据覆盖*/
  INTERNAL_SIZE_T size; /*存储当前块的总大小(字节数)*/

  struct malloc_chunk* fd; /* 前向指针 - 仅空闲时有效 */
  struct malloc_chunk* bk;  /* 后向指针 - 仅空闲时有效 */
  
  struct malloc_chunk* fd_nextsize; /* 大块专用:指向下一个不同大小的块 */
  struct malloc_chunk* bk_nextsize; /* 大块专用:指向上一个不同大小的块 */
}
  • 字段解释:
    • 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的大小以及内存地址
    • fd,bk: chunk处于分配状态时,从fd字段开始是用户的数据。chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
      • fd指向下一个(非物理相邻)空闲的chunk
      • bk指向上一个(非物理相邻)空闲的’chunk`
    • fd_nextsize, bk_nextsize: 也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
      • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
      • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
      • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。
    • chunk结构
      • 栈示意图:

      Canary

      * 一个已经分配的 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`块

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)

  • 错误:在释放了堆上的内存后引用(又名悬垂指针引用)
  • 后果:攻击者可以使用被释放的指针控制数据写入
  • 错误示例:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    int 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);
    }
    
    在该例子中,当buf1被释放后,该内存就立刻可以重用,之后在为buf2和buf3分配空间时可能分配了该内存。使用被释放的指针进行写操作就可能会覆盖buf2和buf3
  • 利用UAF: 覆盖控制流数据
  • 预防UAF: 将被释放的指针设置为NULL

双重释放(Double Free)

  • 示例:
1
2
3
4
5
6
7
8
9
int main(int argc, char** argv)
{
  buf1 = (char*)malloc(BUFSIZE1);
  free(buf1);
  buf2 = (char*)malloc(BUFSIZE2);
  strbncpy(buf2, argv[1], BUFSIZE2-1);
  free(buf1);
  free(buf2);
}
  • 代码工作:
    • 释放buf1,然后分配buf2
      • buf2可能占用buf1相同的内存空间
    • buf2获取用户提供的数据
    • 再次释放buf1
      • 其中可能使用一些buf2数据作为元数据
      • 并且可能打乱buf2的元数据
    • 然后是buf2,此时使用了混乱的元数据
  • 双重释放可以达到与堆溢出漏洞类似的效果,可以使用类似的方式预防

空字节溢出(Off- by-Null)

  • 堆溢出一字节: 将缓冲区改为0
  • 利用方式: 将P从1改写为0
    • 这将导致前一个块被视为空闲
    • 下一个块的释放会将空闲块合并
  • 攻击流程:
    • 分配内存,定位地址
    • 空字节溢出
    • 断链
    • 写入覆盖
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计