snowcat · blog

从零开始的堆利用 0x00

分类PWN系列Heap 1/2

堆利用基础

堆是 虚拟内存空间的一段连续的线性区域 ,提供动态分配的内存,允许程序申请大小未知的内存。在用户与操作系统之间,作为动态内存管理的中间人,响应程序的申请内存请求,向操作系统申请内存,然后返回给程序。并且管理用户所释放的内存,适时归还给操作系统。

glibc 使用 ptmalloc2 作为堆管理器。

申请内存时的系统调用

  • brk -> 小内存
  • mmap -> 大内存

Arena

内存分配区,可以理解为堆管理器所持有的内存池。

堆管理器与程序的内存交易发生在arena中,可以理解为堆管理器向操作系统批发来的由冗余的内存组成的库存。

操作系统 -> 堆管理器 -> 程序 物理内存 -> arena -> 可用内存

在 Linux 中,程序如果直接向操作系统申请内存,需要通过系统调用(比如 brkmmap)。系统调用的代价非常昂贵。为了解决这个问题,堆管理器不会等程序要内存了才去操作系统拿,而是提前向操作系统申请一大块内存,并把这块内存放在 Arena中,当程序需要内存时,堆管理器直接从 Arena 中切下一小块(Chunk)。同样的,被程序释放的内存也不会直接归还操作系统,由ptmalloc统一管理空闲的内存。

Chunk

程序申请内存的单位,堆管理器中管理内存的基本单位。 malloc()函数返回的指针指向一个 chunk 的数据区域

分类

按状态分为

  • malloced: 已经分配且填写了数据的chunk
  • free:被释放掉的malloced chunk成为free chunk

按大小:

  • fast
  • small
  • large
  • tcache

按特定功能:

  • Top chunk:Arena中未被分配使用过的区域
  • last remainder chunk(了解):释放(free)大堆块之后,重新用 malloc 分割 chunk 时剩余的部分

大小 / 结构

堆的大小必须是 MALLOC_ALIGNMENT 的整数倍。如果申请的大小不是,会被转换为相应的最小值,32 位系统中, MALLOC_ALIGNMENT 可能是 0x4 或 0x8 ;64 位系统中,MALLOC_ALIGNMENT 是 0x8 或 0x10。可以发现,不管 size 的大小如何变,size 的二进制低三位都为 0,为了不浪费这三个比特位,他们从高到低分别用来表示( A M P ):

  • NON_MAIN_ARENA:记录- 当前chunk是否不属于主线程,1表示不属于,0表示属于
  • IS_MAPPED:记录当前 chunk 是否由 mmap 分配
  • PREV_INUSE:记录前一个 chunk 是否被使用

chunk结构如下:

  • prev_size:记录上一个 chunk 的大小,当上一个 chunk 为 free chunk 时生效
  • size:
    • size of chunk
    • A
    • M
    • P
  • fd: bin中指向下一个(不一定物理相邻)空闲的chunk
  • bk: bin中指向上一个(不一定物理相邻)空闲的chunk,仅为处于双向链表的bin中的free chunk时生效
  • fd_nextsize:为large free chunk时生效
  • bk_nextsize:为large free chunk时生效

Bin

用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk。当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配的开销。

ptmalloc 采用分箱式方法对空闲的 chunk 进行管理。首先,它会根据空闲的 chunk 的大小以及使用状态初步分为 4 类:fast bins、 small bins、 large bins、 unsorted bin、 在 libc-2.26 之后引入了tcache bins。

相似大小的 chunk 会用链表链接起来。也就是说,在每类 bin 的内部仍然会有多个互不相关的链表来保存不同大小的 chunk。

bin类型大小范围链表数量(数组大小)结构
fast bin0x20-0x807单向链表
tcache bin0x20-0x41064单向链表
small bin0x20-0x3F062双向循环链表
large bin≥0x40063双向循环链表
unsorted bin不限大小1双向循环链表