3.1.6 Linux 堆利用(一)

Linux 堆简介

堆是程序虚拟地址空间中的一块连续的区域,由低地址向高地址增长。当前 Linux 使用的堆分配器被称为 ptmalloc2,在 glibc 中实现。

更详细的我们已经在章节 1.5.8 中介绍了,章节 1.5.7 中也有相关内容,请回顾一下。

对堆利用来说,不用于栈上的溢出能够直接覆盖函数的返回地址从而控制 EIP,只能通过间接手段来劫持程序控制流。

how2heap

how2heap 是由 shellphish 团队制作的堆利用教程,介绍了多种堆利用技术,这篇文章我们就通过这个教程来学习。推荐使用 Ubuntu 16.04 64位系统环境,glibc 版本如下:

请注意,下文中贴出的代码是我简化过的,剔除和修改了一些不必要的注释和代码,以方便学习。另外,正如章节 4.3 中所讲的,添加编译参数 CFLAGS += -fsanitize=address 可以检测内存错误。下载文件

first_fit

这第一个程序展示了 glibc 堆分配的策略,即 first-fit。在分配内存时,malloc 会先到 unsorted bin(或者fastbins) 中查找适合的被 free 的 chunk,如果没有,就会把 unsorted bin 中的所有 chunk 分别放入到所属的 bins 中,然后再去这些 bins 里去找合适的 chunk。可以看到第三次 malloc 的地址和第一次相同,即 malloc 找到了第一次 free 掉的 chunk,并把它重新分配。

在 gdb 中调试,两个 malloc 之后(chunk 位于 malloc 返回地址减去 0x10 的位置):

第一个 free 之后,将其加入到 unsorted bin 中:

第三个 malloc 之后:

所以当释放一块内存后再申请一块大小略小于的空间,那么 glibc 倾向于将先前被释放的空间重新分配。

好了,现在我们加上内存检测参数重新编译:

一个很明显的 use-after-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。

fastbin_dup

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

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

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

三个 malloc 之后:

第一个 free 之后,chunk a 被添加到 fastbins 中:

第二个 free 之后,chunk b 被添加到 fastbins 中:

此时由于 chunk a 处于 bin 中第 2 块的位置,不会被 double-free 的检查机制检查出来。所以第三个 free 之后,chunk a 再次被添加到 fastbins 中:

此时 chunk a 和 chunk b 似乎形成了一个环。

再三个 malloc 之后:

所以对于 fastbins,可以通过 double-free 泄漏出一个堆块的指针。

加上内存检测参数重新编译:

一个很明显的 double-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。

看一点新鲜的,在 libc-2.26 中,即使两次 free,也并没有触发 double-free 的异常检测,这与 tcache 机制有关,以后会详细讲述。这里先看个能够在该版本下触发 double-free 的例子:

fastbin_dup_into_stack

这个程序展示了怎样通过修改 fd 指针,将其指向一个伪造的 free chunk,在伪造的地址处 malloc 出一个 chunk。该程序大部分内容都和上一个程序一样,漏洞也同样是 double-free,只有给 fd 填充的内容不一样。

三个 malloc 之后:

三个 free 之后:

这一次 malloc 之后,我们不再填充无意义的 "DDDDDDDD",而是填充一个地址,即栈地址减去 0x8,从而在栈上伪造出一个 free 的 chunk(当然也可以是其他的地址)。这也是为什么 stack_var 被我们设置为 0x21(或0x20都可以),其实是为了在栈地址减去 0x8 的时候作为 fake chunk 的 size 字段。

glibc 在执行分配操作时,若块的大小符合 fast bin,则会在对应的 bin 中寻找合适的块,此时 glibc 将根据候选块的 size 字段计算出 fastbin 索引,然后与对应 bin 在 fastbin 中的索引进行比较,如果二者不匹配,则说明块的 size 字段遭到破坏。所以需要 fake chunk 的 size 字段被设置为正确的值。

简单地说就是 fake chunk 的 size 与 double-free 的 chunk 的 size 相同即可。

可以看到,伪造的 chunk 已经由指针链接到 fastbins 上了。之后 malloc 两次,即可将伪造的 chunk 移动到链表头部:

再次 malloc,即可在 fake chunk 处分配内存:

所以对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。

fastbin_dup_consolidate

这个程序展示了利用在 large bin 的分配中 malloc_consolidate 机制绕过 fastbin 对 double free 的检查,这个检查在 fastbin_dup 中已经展示过了,只不过它利用的是在两次 free 中间插入一次对其它 chunk 的 free。

首先分配两个 fast chunk:

释放掉 p1,则空闲 chunk 加入到 fastbins 中:

此时如果我们再次释放 p1,必然触发 double free 异常,然而,如果此时分配一个 large chunk,效果如下:

可以看到 fastbins 中的 chunk 已经不见了,反而出现在了 small bins 中,并且 chunk p2 的 prev_size 和 size 字段都被修改。

看一下 large chunk 的分配过程:

当分配 large chunk 时,首先根据 chunk 的大小获得对应的 large bin 的 index,接着判断当前分配区的 fast bins 中是否包含 chunk,如果有,调用 malloc_consolidate() 函数合并 fast bins 中的 chunk,并将这些空闲 chunk 加入 unsorted bin 中。因为这里分配的是一个 large chunk,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。

由于此时 p1 已经不在 fastbins 的顶部,可以再次释放 p1:

p1 被再次放入 fastbins,于是 p1 同时存在于 fabins 和 small bins 中。

第一次 malloc,chunk 将从 fastbins 中取出:

第二次 malloc,chunk 从 small bins 中取出:

chunk p4 和 p5 在同一位置。

这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。该技术最常见的利用场景是我们有一个可以溢出漏洞和一个全局指针。

Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有一些对前后堆块的检查,也是我们需要绕过的:

在解链操作之前,针对堆块 P 自身的 fd 和 bk 检查了链表的完整性,即判断堆块 P 的前一块 fd 的指针是否指向 P,以及后一块 bk 的指针是否指向 P。

malloc_size 设置为 0x80,可以分配 small chunk,然后定义 header_size 为 2。申请两块空间,全局指针 chunk0_ptr 指向 chunk0,局部指针 chunk1_ptr 指向 chunk1:

接下来要绕过 (P->fd->bk != P || P->bk->fd != P) == False 的检查,这个检查有个缺陷,就是 fd/bk 指针都是通过与 chunk 头部的相对地址来查找的。所以我们可以利用全局指针 chunk0_ptr 构造 fake chunk 来绕过它:

可以看到,我们在 chunk0 里构造一个 fake chunk,用 P 表示,两个指针 fd 和 bk 可以构成两条链:P->fd->bk == PP->bk->fd == P,可以绕过检查。另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 prev_size 为 fake chunk 的大小,修改 PREV_INUSE 标志位为 0,将 fake chunk 伪造成一个 free chunk。

接下来就是释放掉 chunk1,这会触发 fake chunk 的 unlink 并覆盖 chunk0_ptr 的值。unlink 操作是这样进行的:

根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于:

这样就通过了 unlink 的检查,最终效果为:

原本指向堆上 fake chunk 的指针 P 指向了自身地址减 24 的位置,这就意味着如果程序功能允许堆 P 进行写入,就能改写 P 指针自身的地址,从而造成任意内存写入。若允许堆 P 进行读取,则会造成信息泄漏。

在这个例子中,由于 P->fd->bk 和 P->bk->fd 都指向 P,所以最后的结果为:

成功地修改了 chunk0_ptr,这时 chunk0_ptrchunk0_ptr[3] 实际上就是同一东西。这里可能会有疑惑为什么这两个东西是一样的,因为 chunk0_ptr 指针在是放在数据段上的,地址在 0x601070,指向 0x601058,而 chunk0_ptr[3] 的意思是从 chunk0_ptr 指向的地方开始数 3 个单位,所以 0x601058+0x08*3=0x601070

所以,修改 chunk0_ptr[3] 就等于修改 chunk0_ptr

这时 chunk0_ptr 就指向了 victim_string,修改它:

成功达成修改任意地址的成就。

最后看一点新的东西,libc-2.25 在 unlink 的开头增加了对 chunk_size == next->prev->chunk_size 的检查,以对抗单字节溢出的问题。补丁如下:

具体是这样的:

回顾一下伪造出来的堆:

这里有三种办法可以绕过该检查:

  • 什么都不做。

    • chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x0

    • prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x0) == 0x0

  • 设置 chunk0_ptr[1] = 0x8

    • chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x8

    • prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x8) == 0x8

  • 设置 chunk0_ptr[1] = 0x80

    • chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x80

    • prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x80) == 0x80

好的,现在 libc-2.25 版本下我们也能成功利用了。接下来更近一步,libc-2.26 怎么利用,首先当然要先知道它新增了哪些漏洞缓解措施,其中一个神奇的东西叫做 tcache,这是一种线程缓存机制,每个线程默认情况下有 64 个大小递增的 bins,每个 bin 是一个单链表,默认最多包含 7 个 chunk。其中缓存的 chunk 是不会被合并的,所以在释放 chunk 1 的时候,chunk0_ptr 仍然指向正确的堆地址,而不是之前的 chunk0_ptr = P = P->fd。为了解决这个问题,一种可能的办法是给填充进特定大小的 chunk 把 bin 占满,就像下面这样:

现在 libc-2.26 版本下也成功利用了。tcache 是个很有趣的东西,更详细的内容我们会在专门的章节里去讲。

加上内存检测参数重新编译,可以看到 heap-buffer-overflow:

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 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。

首先 malloc(1) 用于初始化内存环境,然后在 fake chunk 区域伪造出两个 chunk。另外正如上面所说的,需要一个传递给 free 函数的可以被修改的指针,无论是通过栈溢出还是其它什么方式:

伪造 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 大小的检查。

libc-2.23 中这些检查代码如下:

mem 就是我们所控制的传递给 free 函数的地址。其中下面两个函数用于在 chunk 指针和 malloc 指针之间做转换:

NON_MAIN_ARENA 为零时返回 main arena:

这样,程序就顺利地进入了 _int_free 函数:

其中下面的宏函数用于获得 next chunk:

然后修改指针 a 指向 (fake chunk 1 + 0x10) 的位置,即上面提到的 mem。然后将其传递给 free 函数,这时程序就会误以为这是一块真的 chunk,然后将其释放并加入到 fastbin 中。

这时如果我们 malloc 一个对应大小的 fast chunk,程序将从 fastbins 中分配出这块被释放的 chunk。

所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的。

该技术的缺点也是需要对栈地址进行泄漏,否则无法正确覆盖需要释放的堆指针,且在构造数据时,需要满足对齐的要求等。

加上内存检测参数重新编译,可以看到问题所在,即尝试 free 一块不是由 malloc 分配的 chunk:

house-of-spirit 在 libc-2.26 下的利用可以查看章节 4.14。

参考资料

Last updated

Was this helpful?