堆溢出漏洞

实验目的

根据一个重复释放漏洞利用例子,调试利用程序(exp.py),理解堆溢出的原理和利用过程中堆的变化。

步骤

程序运行

程序运行后大致显示了如下结果。

静态分析

把程序拖入 IDA 中进行分析,发现主要的逻辑还是比较简单的。

左上图为主要的程序逻辑,用户输入的选择数字对应了不同的子程序,其中输入 1 将进入 sub_400909,对应了 new 操作,从中可以得到,我们最多可以分配 10 个 chunk,每一块最多 96 字节大小。输入 2 将进入 sub_4009f9,对应了 write 操作,它也经历了严格的边界检查,利用基本上不可能。输入 3 将进入 sub_400aef,对应了 delete 操作,其中第 18 行将指针 ptr[v1] free 掉,在第 19 行把其中内容置空了,却没有把指向它的指针置空,留下了双重释放的漏洞。

事实上,尝试连续释放两次相同的 chunk,的确会引发系统的 core dump。如图。

动态分析 (gdb 调试)

  1. 当程序执行到第一个断点时,exp.py 做了这些事情:

    1. 新建了两个 0x60 大小的 “chunk”(恰好为边界值)
    2. 释放第一个堆块,释放第二个堆块,再释放第一个堆块(触发 Double Free),完成 fastbin attack。此时 heap 和 fastbin 的情况如图所示。
      从中可以推断,堆的当前情况是:
      1. 堆的头部为 0x1a740e0
      2. 有两个大小为 0x71(属于 0x70 类)的 fastbin 已被分配,它们目前处于首尾相连状态。因为从截图可以得知,0x1a74000 的前向指针指向了 0x1a740700x1a74070 的前向指针指向了 0x1a74000,刚好是一个闭环。
  2. 当程序执行到第二个断点时,exp.py 做了这些事情:

    1. 分配了第三个堆块,大小为 0x60。

    2. 向其中写入地址 0x60208d

      • 值得注意的是在每次分配堆块后,有一个全局变量的数字均自增了 1,这个全局变量在每次分配堆块前均和 9 检查,证明一共最多只能进行 10 次分配操作,10 次分配,在 ptmalloc 分配方式中只有 fastbin 中有 10 个堆块满足我们的要求,这也是采用 fastbin attack 的原因。
      • 下图显示堆块 0x1a74000 的前向指针 fd 被篡改为了我们所写的地址 0x60208d 上,而 0x1a74010000000,一个内存地址正在指向它。回环结构已被破坏。

      为什么是 0x60208d

      我们需要一个大小属于 0x70 类的空闲块来满足 malloc 安全分配的需求(程序中要求最大分配 0x60 的 chunk),观察内存,可以发现 0x60208d 为满足要求的内存块之一。

      由于小端显示原因,此图显示实际上对应于内存中的顺序为:第二列 - 第一列 - 第四列 - 第三列,与 gdb-peda 的显示不太相同。

    这次分配第三个堆块的意义是将所找到可以利用的内存地址推入 FASTBIN 链中,后续可以方便直接对内存中此处内容进行改写。

  3. 当程序执行到第三个断点时,exp.py 做了如下事情:

    1. 分配了大小为 0x60 的第四、第五、第六块 chunk。
    2. 向第五块 chunk 写入字符串 /bin/sh\x00
    3. 向第六块 chunk 写入 got 表中 free 的地址。
      • 这时 fastbin 中只剩下一个大小为 0x70 的块:0x1a74010000000,原来的 0x1a74000 已经指向了一个内存地址中。

      • 查看现在 0x602080 的内容,发现已经被我们所写入的 got 中 free 表地址所覆盖。哦,原来 0x00602018 是 got 表中 free 的地址啊!

  4. 当程序执行到第四个断点时,exp.py 做了如下事情:

    • 向第三块 chunk 中写入 plt 表中 system 的地址(第三块没有被 free 过,现在实际上在覆盖写)。
    • Bin 和 heap 相较上一步没什么变化。
  • 最终,我们释放了含有 /bin/sh 字符串的第五块 chunk,由于 free 的 got 表地址已经被改写成了 system 的 plt 表地址,再调用 free 就相当于调用了 system,而被释放的块中含有 /bin/sh 字符串,所以就相当于执行了 system("/bin/sh"),成功获取 shell。

实验难点与收获

堆的结构比较复杂,需要的前置知识非常多,理解起来也会非常难。通过学习这次的利用程序,我掌握了大概的利用思路。

其实应该再加一个断点,来观察一下 free 后 bin 和 heap 的情况,不过多半因为程序控制权转移而看不到这里的现象。

实际上,所有的利用办法,最终都是要想办法执行 system("/bin/sh") 的,但是利用的路线是不相同的。

实验思考

为什么可以在 0x602018 处发现符合要求的地址,大概是因为有一个在 dword_6020f0 的关键全局变量,所以可以推断可供利用的内存地址、全局变量等数据都会在其附近?(然后逐渐缩小查找范围……)

附录:exp.py 源文件