常见堆利用手法解析(中)

渗透技巧 1年前 (2022) admin
348 0 0

 作者:Ally Switch


前言


本文接《常见堆利用手法解析(上)》,点击蓝字可跳转?


学习材料:shellphish 团队在 Github 上开源的堆漏洞系统教程 “how2heap”。

glibc版本:glibc2.31

操作系统:Ubuntu 20.04

示例选择:本篇参考了pukrquq师傅的分析文章,选用了在高版本依然存在的利用方式进行分析。

编译选项:gcc xxx.c -o xxx -g,添加 -g 选项以创建调试符号表,并关闭所有的优化机制。


常见堆利用手法解析(中)


常见堆利用手法解析(中)

01 fastbin_reverse_into_tcache


1. 源码分析


由于源码较长,本篇中的 poc 示例会先贴出源码,删减掉不影响程序逻辑的代码后,再对简化后的版本进行分析。


完整版:

常见堆利用手法解析(中)


简化版:

常见堆利用手法解析(中)


·  橙色方框:

  a. malloc() 14 个可以容纳 0x40 大小的内存块(即大小 0x50 的 chunk);

   b. 先 free() 7 个以填满 0x50 大小的 tcache bin;

   c. 再 free() 1 个到 0x50 大小的 fast bin 中,free() 前获取它的指针保存为 victim;

   d. 再 free() 6 个到 0x50 大小的 fast bin 中,fast bin 采用头插法入链,victim 会变成链上最后一个 chunk。


·  蓝色方框:

   a. 初始化一个位于栈中的数组;

   b. 填充 0xcd 用于实验后的对照。


·  红色方框:

   a. victim 指向的值就是 victim 这个 chunk 对应的 fd 指向的值,由于 victim 位于 fast bin 末尾,因此其指向 NULL;

   b. 设置 victim 指向 stack_var[0] 对应的地址;

   c.  此操作,相当于把 stack_var[0] 所在地址作为 chunk 首地址放入 fast bin 中;

   d.  换句话说,stack_var 此时成为 fast bin 中的最后一个 chunk,而 victim 则是倒数第二个 chunk,此时 fast bin 中有 8 个 chunk。


·  粉色方框:

   a.  先 malloc() 7 个可以容纳 0x40 大小的内存块,清空 tcache;

   b.  打印一下 stack_var 这个数组中的值,目前应该一切正常,全部为 0xcd;

  c.  再 malloc() 1 个可以容纳 0x40 大小的内存块,回顾一下此时 malloc() 会进行哪些操作:

         i.   由于 0x50 大小的 tcache bin 已经空了,所以会进入 _int_malloc() 进行分配;

       ii.  _int_malloc() 会先找 fast bin,发现 fast bin 中有符合要求的 chunk;

         iii.   这时会先把符合要求的 chunk 从 fast bin 中取下来,稍后会返回给用户;

         iv.   进入一个循环,对 fast bin 中剩下的 chunk 进行遍历,把 0x50 大小的 chunk 全部放入 0x50 大小的 tcache bin 中,直到 tcache bin 满了(达到 7 个)或 fast bin 空了,循环才停止;

       v.   然后把先前取下来的 chunk 返回给用户。

   d.  在实际场景中,经过上述操作:

      i.     由于 victim 在 fast bin 是倒数第二个,stack_var 是倒数第一个,因此不会率先返回给用户;

       ii.     fast bin 上的第一个 chunk 会被取下,用于稍后返回,此时 fast bin 上还剩下 7 个 chunk;

       iii.     接下来,这 7 个 chunk 会在一个循环中,被放入到 0x50 大小的 tcache bin 中;

      iv.  stack_var 这个 chunk 会最后从 fast bin 被取出来,进入 tcache bin 后会成为第一个;

       v.     此时由于 tcache_put() 会设置 chunk 的 next 与 key 两个字段,因此理论上 stack_var[2] 与 stack_var[3] 处会被赋值。

   e.  然后就是对照实验,再打印一下 stack_var,会发现,此时 stack_var[2] 与 stack_var[3] 对应的值应该已经被修改;

    f.   最后再 malloc() 一下,由于 stack_var 这个 chunk 位于 tcache bin 的开头,所以我们拿到的就是 stack_var 这个 chunk,返回的指针也就指向 stack_var[2] 的位置。

2. 执行分析

常见堆利用手法解析(中)
  • 这里比较直观的感受就是 stack_var 的值确实变了,变的也确实是 stack_var[2] 与 stack_var[3] 位置;

  • 这大概也能推论出 stack_var 确实是进入 bin 中了,但仍无法看出 tcache bin 与 fast bin 中发生的变化,这需要进一步调试。

3. 调试分析


1. free() victim 后

常见堆利用手法解析(中)

可以看到,此轮运行,victim 对应的 chunk 地址为 0x5555555594c0。


2. 14 个块全部 free()

常见堆利用手法解析(中)

由于我环境中 pwndbg 的 bins 指令显示不全,我也不清楚是怎么回事,这里只好用 heap 指令查看堆空间。


3. 设置了 victim 指向 stack_var 后

常见堆利用手法解析(中)

可以看到此时 victim 的 fd 被设置成了栈中的地址,也就是 stack_var。


4. malloc() 触发 fastbin reverse into tcache

常见堆利用手法解析(中)

最后可以看到,fast bin 中的 chunk 进入 tcache bin 中,并且 stack_var 成了 tcache bin 中的第一个 chunk,此时只需再 malloc() 一次就可以将其取出。

原理小结


  • 利用手法是通过溢出等手段,修改进入 fast bin 中 chunk 的 fd 指针,指向一个伪造的 chunk,实现任意地址覆盖。

  • 原理是当进入 _int_malloc() 中分配 chunk 时,如果在 fast bin 中发现了合适的 chunk ,那么会将剩余的 chunk 填入 tcache bin 中。


常见堆利用手法解析(中)

02 tcache_stashing_unlink_attack


1. 源码分析


完整版:

常见堆利用手法解析(中)
常见堆利用手法解析(中)


简化版:

常见堆利用手法解析(中)


·  橙色方框:

   a.  先 malloc() 9 个可以容纳 0x90 大小的内存块,即 9 个 0xa0 大小的 chunk;

   b.  free() 掉其中 7 个去填满 0xa0 大小的 tcache bin;

   c.  这里有个细节,剩下 2 个还没 free() 掉的,在堆上是不连续的,也就是说,这样进入 unsorted bin 后,他俩就不会合并了。因为 tcache bin 中的 chunk 本身是处于 inuse 状态,因此不会与 unsorted bin 中的 chunk 进行合并;

   d.  接下来把剩下俩给 free() 掉,由于大小为 0xa0,是不会进入 fast bin 的(最大支持 0x80 的 chunk),所以会进入 unsorted bin,注意 unsorted bin 进入时会放在 bin 与 bin->fd 之间,取的是 bin 与 bin->bk->bk 之间的 chunk,所以是先进先出(FIFO),small bin 同理。


·  蓝色方框:

   a.  第一次 malloc() 一个可以容纳 0xa0 大小的 chunk(即 0xb0 大小的 chunk),显然此时堆中没有合适大小的 chunk,所以最终会通过 top chunk 进行分配,但是在 _int_malloc() 的查找过程中,会将 unsorted bin 中的 chunk 放入对应的 bin 中;

   b.  因此先前 free() 掉的 chunk_lis[0] 与 chunk_lis[2] 这俩指针对应的 chunk 就进入了 small bin;

   c.  small bin 与 unsorted bin 都是先进先出,因此这波操作完 small bin 大概是:smallbin(0xa0) <-> chunk_lis[2] <-> chunk_lis[0];

   d.  然后再 malloc() 两个 0xa0 的 chunk,用于在 tcache bin 中腾出位置。


·  红色方框:

   a.  核心操作都在这了,先给 stack_var[3] 赋值为 &stack_var[2],这是将 stack_var 伪造成一个 fake chunk,其中 stack_var[3] 表示这个 fake chunk 的 bk,此时赋值为一个合理存在的地址,仅此而已,用于过掉 glibc 中的校验,具体咋过,后面会介绍到;

  b.  这里将 stack_var 这个 fake chunk 赋值给 chunk_lis[2][1],chunk_lis[2] 本身位于 fd 的位置,chunk_lis[2][1] 则是 bk 的位置,所以这一步,就是将 chunk_lis[2] 在 small bin 中对应的 chunk 的 bk 设置为了 stack_var,上面这两步的操作变化大概是这样:

常见堆利用手法解析(中)

   c.  接下来,进行了一次 calloc() 的操作来申请内存块,calloc() 不会访问 tcache bin,而是直接进入 _int_malloc();

   d.  根据先前的知识,small bin 是先进先出,因此先进入 small bin 的 chunk_lis[0] 会被取出来分配给用户;

   e.  接下来,_int_malloc() 中会将 small bin 中余下的 chunk 往对应大小(也就是 0xa0)的 tcache bin 中放,由于 tcache bin 中目前已经有 5 个 chunk 了,因此最多只能再放 2 个,按照先进先出的顺序,chunk_lis[2] 会被先行放入 chunk 中,接下来,会进行如下判断(下面放上源码):

        i.   chunk_lis[2]->bk 是否为链表头 bin,此时这个值为 stack_var(默认情况下应该是链表头 bin);

         ii.   然后会再判断一次这个值是否为空;

        iii.   若不为空,说明这是一个 chunk,则获取这个 chunk 的 bk,也就是这里 fake chunk 的 bk,将其赋值给 bck。前面设置了这个 bk 的值为一个合理存在的地址,也就是为了过掉这里的校验,不然 bck 的值不合法,那么后面执行 bck->fd = bin 可能就出错了。由于 tcache bin 的容量是 7 个,此时 fake chunk 会在 chunk_lis[2] 之后进入 tcache bin 中,并位于第一个。

常见堆利用手法解析(中)


·  粉色方框:

   a.  最后再 malloc() 一下,因为 tcache bin 中的第一个位置已经是我们伪造的 fake chunk,自然也就获取到了这个 chunk。

2. 执行分析

常见堆利用手法解析(中)
  • 这里可以看出最后一次 malloc() 得到的是一个栈中的地址,并且刚好是 fake_chunk->bk 处的值,因为这里保存的值为 fake_chunk[2] 的地址,也就是 fake_chunk 的 fd 的位置。

3. 调试分析


1. 9 个内存块都 free() 

常见堆利用手法解析(中)

可以看到,此时 0xa0 大小的 tcache bin 已满,unsorted bin 中有两个 chunk。


2. malloc(0xa0)

常见堆利用手法解析(中)

此时 unsorted bin 中的两个 chunk 已进入到 0xa0 大小的 small bin 中。


3. 两次 malloc(0x90)

常见堆利用手法解析(中)

两次 malloc() 给 tcache bin 中腾出了两个位置。


4. 设置 fake chunk 后

常见堆利用手法解析(中)

此时 chunk_lis[2] -> bk 处的值已经被修改为 fake chunk 的地址。


5.  calloc() 执行后

常见堆利用手法解析(中)

此时 fake chunk 已进入 0xa0 大小的 tcache bin 中。

原理小结


  • 本例和前面的

    fastbin_reverse_into_tcache 很像,这里也是通过修改 small bin 然后利用 small bin 进入 tcache bin 来实现任意地址写

  • unsorted bin 与 small bin 都是双链表,且都是先进先出,都是进入时插入 bin 与 bin->fd 之间,取的是 bin 与 bin->bk->bk 之间的 chunk

  • 因此修改 small bin 与前一个例子修改 tcache 不太一样,这里修改的是 victim->bk 的值

  • 造成漏洞的原因主要是 glibc 中将 small bin 导入 tcache bin 时,只检查了 tc_victim->bk 这个指针,没有检查链表完整性,从而有了利用的空间

常见堆利用手法解析(中)

03 house_of_botcake


1. 源码分析


完整版:

常见堆利用手法解析(中)
常见堆利用手法解析(中)


简化版:

常见堆利用手法解析(中)


·  橙色方框:

   a.  定义一个数组,位于栈中,用作后续 malloc() 的返回值,也可以理解为本例中的 fake chunk;

   b.  初始化 7 个 0x110 大小的 chunk,用于填充 tcache bin;

   c.  malloc() 1 个 0x110 大小的 chunk,用于 free() 后的块合并;

   d.  malloc() 1 个 0x110 大小的 chunk,作为 victim chunk;

  e.  malloc() 1 个 0x20 大小的 chunk,作为 padding,防止 victim chunk 与 top chunk 合并。


·  蓝色方框:

   a.  free() 掉 7 个 0x110 大小的 chunk,填充满 0x110 大小的 tcache bin;

   b.  首次 free() 掉 victim chunk,victim chunk 此时会进入 unsorted bin;

   c.  free() 掉用于合并的 chunk,它会与上一步 free() 掉的 victim chunk 进行合并,形成一个 0x220 大小的 chunk,然后进入 unsorted bin 中;

   d.  malloc() 一个 0x110 大小的 chunk,使得 tcache bin 中腾出一个位置。


·  红色方框:

   a.  第二次 free() 掉 victim chunk,第二次 free 的 victim chunk 会进入刚刚腾出位置的 0x110 大小的 tcache bin 中;

   b.  malloc() 一个 0x130 大小的 chunk,由于 tcache bin 中没有符合的,fast bin 与 small bin 也没有符合大小的;

    c.  所以会进入 unsorted bin 的处理流程,此时:

           i. unsorted bin 中刚好只有一个 

chunk;

          ii.  分割后的剩余大小也大于 

MINSIZE;

         iii.  这个 chunk 符合 small bin 的范围;

         iv.  同时也是 last remainder chunk。

    d.  因此,分配器就会从这个 0x220 的 chunk 里面分出 0x130 大小返回给用户;

     e.  接下来修改刚刚拿到的这个 

chunk[0x130 – 0x8*2] 的位置,原先合并前两个 chunk 大小都是 0x110,这里修改 0x120 位置的值为 stack_var,也就是修改了原先第二个 chunk 的 fd 指针的位置,而这个被修改的 fd 的 chunk 也就是 victim chunk,它已经经过 double free 被放在了 tcache bin 里;

    f.   在修改 fd 指针后,相等于修改了 tcache bin 中 victim chunk 的后一个 chunk,将其设置为了 stack_var,此时 tcache bin 中的状态大概为:


常见堆利用手法解析(中)


·  粉色方框:

   a.  先 malloc() 一个 0x110 的 chunk,把 victim chunk 给申请走;

   b.  再 malloc() 一个 0x110 的 chunk,就可以拿到一个 &stack_var[-2] 开始的 chunk 了,返回的指针刚好是 stack_var。

2. 执行分析

常见堆利用手法解析(中)
  • 执行结果看不出什么东西,只能看出最后成功申请了一个栈空间的地址。

3. 调试分析


1. unsorted bin 中 chunk 合并后

常见堆利用手法解析(中)

合并前,unsorted bin 中的 chunk 位于 0x555555559b10;

合并后,unsorted bin 中的 chunk 位于 0x555555559a00。


2. double free 后

常见堆利用手法解析(中)

double free 后发现 tcache bin 中出现了合并前的那个 chunk,之所以是 0x555555559b20,是因为 tcache bin 中是用 fd 来链接 chunk。


3. unsorted bin 中 chunk 分割后

常见堆利用手法解析(中)

分割后,unsorted bin 中的 chunk 变成了 0x555555559b30;

用新申请的 chunk 修改的位置则是 0x555555559b20,也就是合并前的 victim chunk 的 fd。


4. 修改 victim chunk 的 fd 后

常见堆利用手法解析(中)

修改 victim chunk 的 fd 后发现 tcache bin 中的第二个 chunk 已经变成了我们可以控制的栈中的地址。

原理小结


  • 原理非常简单,double free 实现 tcache bin 与 unsorted bin 中包含相同 chunk

  • 利用分配内存时会对 unsorted bin 的 last remainder chunk 进行分割,获取到 victim chunk 前面地址的一个 chunk,然后利用覆盖实现任意地址写入


常见堆利用手法解析(中)

04 large_bin_attack


1. 源码分析


完整版:

常见堆利用手法解析(中)


常见堆利用手法解析(中)


简化版:

常见堆利用手法解析(中)


·  橙色方框:

   a. 定义一个局部变量 target,位于栈中;

   b. malloc() 一个 0x430 大小的 chunk,然后再 malloc() 一个 0x20 大小的 padding 防止 free() 时与 top chunk 合并;

   c. malloc() 一个 0x420 大小的 chunk(略小于前一个 chunk,但属于 large bin 的范围内,且与前一个 chunk 位于同一个 idx 对应的 large bin 上),然后再 malloc() 一个 0x20 大小的 padding 防止 free() 时与 top chunk 合并。


·  蓝色方框:

   a. free() 掉 0x430 大小的 chunk 进入 unsorted bin;

   b. malloc() 一个 0x440 大小的 chunk,使得 unsorted bin 处理流程中将 0x430 大小的 chunk 传入 large bin 中;

   c. free() 掉 0x420 大小的 chunk 进入 unsorted bin;

   d. 此时,unsorted bin 中有一个 0x420 大小的 chunk,large bin 中有一个 0x430 大小的 chunk。


·  红色方框:

   a. 将 p1[3] 位置的值设置为 &target – 0x20;

   b. p1[3] 这个位置就是 0x430 大小的 chunk 对应的 bk_nextsize;

   c. 这个操作相当于 p1->bk_nextsize = &target – 0x20。


·  粉色方框:

   a. 此时再 malloc() 一个 0x440 大小的 chunk,该操作再次触发 unsorted bin 处理流程,使得 0x420 大小的 chunk 传入 large bin 中;

   b. 从 unsorted bin 进入 large bin 中的过程有 4 种可能的情况,具体可以参考 malloc源码分析-Unsorted Bin处理流程,此处 0x420 大小的 chunk 进入 large bin 属于文中描述的情形二,具体在源码中为下图框出部分:

常见堆利用手法解析(中)

   c. 红框中圈出了关键的两个运算:

      i. victim->bk_nextsize = 

         fwd->fd->bk_nextsize:

     1) fwd->fd 指向的就是原本位于 

          large bin 中的那个 0x430 大小的 chunk;

      2)这个 chunk 的 bk_nextsize 

          已经设置为了 &target – 0x20;

      3)victim 就是刚刚准备进入 

          large bin 的0x420 大小的 chunk;

      4)这里就是将 victim 的 bk_nextsize 设置为了 &target – 0x20。

      ii.  victim->bk_nextsize->fd_nextsize = victim:

      1)将 victim->bk_nextsize 换成 &target – 0x20;

      2)对于 large bin chunk 来说,

         fd_nextsize 的位置就是 chunk首地址 + 0x20/ 返回指针ptr + 0x10 的位置;

      3)假设我们把 fd_nextsize 以 chunk首地址 + 0x20 代入,可以得到 &chunk = victim;

      4)也就是说,此时 target 所在的这个地址,被赋上了 victim 对应 chunk 的首地址(0x420 大小的 chunk,对应指针 p2);

       5)我们知道,返回指针位于 chunk首地址 + 2*SIZE_SZ 的位置,即可满足示例中 p2 – 2*0x8 == target 这个逻辑。

2. 执行分析

常见堆利用手法解析(中)
  • 这个 poc 的描述语句也是比较细的,可以清晰的看到,位于栈中的局部变量 target,原本保存的值为 0,之后被替换成了 p2 所对应的 chunk 的首地址(源码中使用的都是 p2-2*SIZE_SZ 来表示 p2)。

3. 调试分析


1. 0x430 大小的 chunk 进入 large bin 后

常见堆利用手法解析(中)

进入 large bin 后,这个 0x430 大小的 chunk 位于 0x555555559290。


2. 0x420 大小的 chunk 进入 unsorted bin 后

常见堆利用手法解析(中)

进入 unsorted bin 后,这个 0x420 大小的 chunk 位于 0x5555555596e0,因为是后申请的,所以位置更靠近 top chunk,由于都申请了 padding,所以不会与 top chunk 合并。


3. 修改 0x430 对应 chunk 的 bk_nextsize 后

常见堆利用手法解析(中)

这步比较关键,但是 bins 看不出来,直接用 heap 指令看,可以发现:

  • 修改了位于 large bin 中这个 0x430 大小的 chunk 的 bk_nextsize,设置成了一个栈中的地址,正是 &target – 0x20 的位置;

  • 修改前,由于 large bin 中只包含这一个 chunk,因此 fd_nextsize/bk_nextsize 都指向自身。


4. 0x420 大小的 chunk 进入 large bin 后


常见堆利用手法解析(中)

将 0x420 大小的 chunk 从 unsorted bin 移入 large bin 后:

  • 0x420 大小的这个 chunk 的 bk_nextsize 的值修改为了 &target – 0x20 这个地址,这是执行 victim->bk_nextsize = fwd->fd->bk_nextsize 这条语句后的结果;

  • &target 地址处(0x7fffffffdd40)保存的值已经变成了 0x420 这个 chunk 的首地址,这是执行 victim->bk_nextsize->fd_nextsize = victim 这条语句后的结果,注意这条语句中的 victim,正是 0x420 这个 chunk 的首地址。

原理小结


  • 利用了 unsorted bin 中的 chunk 进入 large bin 时校验不全的漏洞;

  • 当进入 large bin 的 chunk 小于这条 bin 上的所有 chunk 时,则会放在一个边缘的位置。此时原先最小的 chunk,会与当前准备放入的 chunk 通过 fd_nextsize/bk_nextsize 进行链接,但是 glibc 中未对原先最小的 chunk 的 bk_nextsize 的值进行校验,如果我们修改了这个值(例如改成了一个地址 0x7fffffff0000),那么在经过 glibc 中的运算后,这个 0x7fffffff0020(0x7fffffff0000 -> fd_nextsize) 这个地址就会存着先前准备放入的 chunk 的首地址。这样就获取了一个可以编辑的地址。


常见堆利用手法解析(中)
常见堆利用手法解析(中)

未完待续…



原文始发于微信公众号(中孚安全技术研究):常见堆利用手法解析(中)

版权声明:admin 发表于 2022年10月13日 下午6:30。
转载请注明:常见堆利用手法解析(中) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...