pwn之堆学习入门系列

有关堆的基础知识:

Linux堆内存管理深入分析(上)

关于heap overflow的一些笔记:

https://etenal.me/archives/1121#top_chunk

heap堆结构及相关漏洞类型:

https://joyceqiqi.wordpress.com/2017/05/30/heap%e5%a0%86-%e5%9f%ba%e7%a1%80%e7%9f%a5%e8%af%86/

PWN之堆内存管理:

https://paper.seebug.org/255/

Dance In Heap(一)浅析堆的申请释放及相应保护机制:

http://www.freebuf.com/articles/system/151372.html

堆操作可视化工具:

https://github.com/wapiflapi/villoc

有关堆的套路题

how2heap总结-上:

https://www.anquanke.com/post/id/86808

PWN学习之house of系列(一):

https://paper.seebug.org/521/

how2heap-01 first fit实践笔记:

https://vancir.com/2017/08/04/how2heap-01-first-fit/

image.png

** 各种流程图(来自http://www.cnblogs.com/shangye/):

glibc-2.23_large_bin_链接方式浅析

glibc-2.23_int_free_流程浅析

glibc-2.23_int_malloc_流程浅析

利用知识点:

malloc_chunk 的结构

/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /*记录前一个chunk的大小*/
  INTERNAL_SIZE_T      size;       /* 记录本chunk的大小(包括头部) */

  struct malloc_chunk* fd;
  struct malloc_chunk* bk;

  struct malloc_chunk* fd_nextsize;
  struct malloc_chunk* bk_nextsize;
};

一般来说,size_t 在 64 位中是 64 位无符号整数,32 位中是 32 位无符号整数。

  • prev_size, 如果该 chunk 的物理相邻的前一地址chunk(两个指针的地址差值为前一chunk大小)是空闲的话,那该字段记录的是前一个 chunk 的大小(包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk
  • size,该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1表示不属于,0表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的P位都会被设置为1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并。
  • fd,bk, chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适chunk 时挨个遍历。

相关机制规则

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前chunk使用。这就是chunk中的空间复用

最小申请的堆内存大小

用户最小申请的内存大小必须是 2 * SIZE_SZ 的最小整数倍,也就是每个chunk至少得包含fd和bk

global_max_fast

ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就是设置 fast bins 中 chunk 的最大值。当 MAX_FAST_SIZE 被设置为 0 时,系统就不会支持 fastbin 。

因此有可能通过操作这个全局变量来实现将fastbin的范围改大,从而实现骚操作

unsorted bin的来源

unsorted bin 处于我们之前所说的 bin 数组下标 1 处。故而 unsorted bin 只有一个链表。unsorted bin 中的空闲 chunk 处于乱序状态,主要有两个来源

  • 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
  • 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中

初始情况下,我们可以将 unsorted chunk 作为 top chunk,初始状态下unsorted chunk存的是top chunk的地址

只有不是 fast bin 的情况下才会触发unlink

为了避免heap中有太多零零碎碎的内存块,合并之后可以用来应对更大的内存块请求。合并的主要顺序为

  • 先考虑物理低地址空闲块
  • 后考虑物理高地址空闲块

合并后的 chunk 指向合并的 chunk 的低地址。

first-fit

在分配内存时,malloc 会先到 unsorted bin(或fastbins) 中查找适合的被 free 的 chunk,如果没有,就会把 unsorted bin 中的所有 chunk 分别放入到所属的 bins(small_bins和large_bins)中,然后再去这些 bins 里去找合适的 chunk。

fastbins : double-free

可以泄漏出一块已经被分配的内存指针。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。

libc-2.23 中对 double-free 的检查过程如下:

    /* Check that the top of the bin is not the record we are going to add
       (i.e., double free).  */
    if (__builtin_expect (old == p, 0))
      {
        errstr = "double free or corruption (fasttop)";
        goto errout;
      }

它在检查 fast bin 的 double-free 时只是检查了第一个块。所以其实是存在缺陷的。

可利用来泄漏一个地址的内容,或者实现往该地址中写入内容,bss段或者栈都行

需要注意的时候该方法仅仅在fastbin的范围内可用,因为fastbin在free后的p标志位也一样是1,不会进行unlink的操作

PS:在 libc-2.26 中,即使两次 free,也并没有触发 double-free 的异常检测,这与 tcache 机制有关

house-of-spirit

house-of-spirit是一种 fastbins 攻击方法,通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址,即任意我们可控的区域。

house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法,一般的栈溢出漏洞的利用都希望能够覆盖函数的返回地址以控制 EIP 来劫持控制流,但如果栈溢出的长度无法覆盖返回地址,同时却可以覆盖栈上的一个即将被 free 的堆指针,此时可以将这个指针改写为栈上的地址并在相应位置构造一个 fast bin 块的元数据,接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。

当然也不一定是仅用于改写栈的内容,可根据题目的具体灵活运用,改bss,函数got表等等

所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。

另外需要注意的是:

伪造 chunk 时需要绕过一些检查,首先是标志位,PREV_INUSE 位并不影响 free 的过程,但 IS_MMAPPED 位和 NON_MAIN_ARENA位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 2*SIZE_SZ(即大于16),小于 av->system_mem(即小于128kb),才能绕过对 next chunk 大小的检查。

off_by_one

也就是溢出一个字节的各种操作,大概分以下几种:

  • 扩展被释放块:当溢出块的下一块为被释放块且处于 unsorted bin 中,则通过溢出一个字节来将其大小扩大,下次取得次块时就意味着其后的块将被覆盖而造成进一步的溢出
  • 扩展已分配块:当溢出块的下一块为使用中的块,则需要合理控制溢出的字节,使其被释放时的合并操作能够顺利进行,例如直接加上下一块的大小使其完全被覆盖。下一次分配对应大小时,即可取得已经被扩大的块,并造成进一步溢出

  • 收缩被释放块:此情况针对溢出的字节只能为 0 的时候,也就是本节所说的 poison-null-byte,此时将下一个被释放的块大小缩小,如此一来在之后分裂此块时将无法正确更新后一块的 prev_size 字段,导致释放时出现重叠的堆块

  • house of einherjar:也是溢出字节只能为 0 的情况,当它是更新溢出块下一块的 prev_size 字段,使其在被释放时能够找到之前一个合法的被释放块并与其合并,造成堆块重叠

堆漏洞的系列操作

  • 检查是否有UAF漏洞,也就是看malloc的时候和free的时候对应的堆指针有没有可能重复使用
  • 利用fastbin中的double_free来泄漏一个地址的内容,或者实现往该地址中写入内容
  • 一般进行堆的各种操作的时候都需要先建立一个堆来防止后面free操作的时候与top_chunk合并
  • 利用unsorted bin的fd或者bk进行泄漏libc或者堆地址
  • 利用unlink漏洞进行任意地址写,前提是非fastbin,需要知道heap地址和可利用的uaf指针
  • 修改top chunk的大小,进行下一次分配时可造成任意地址写

堆的题目一般如果要getshll的话,基本上通过改got表进行操作,如果是改函数的got表为system,则还需要参数“/bin/sh”,或者将某些程序中调用程序的真正地址改为one_gadget(比如malloc_hook)