首页
社区
课程
招聘
[翻译]Malloc Maleficarum之The House of Prime
2017-3-7 20:24 5393

[翻译]Malloc Maleficarum之The House of Prime

2017-3-7 20:24
5393

2001年底,"Vudo Malloc Tricks""Once Upon A free()"定义了Linux中的动态内存块溢出的开发。在2004年底,一系列关于GNU libc malloc的补丁实现了十多个强制性完整性断言,有效地地说明了现有的技术已经过时了。

正是由于这个原因,一个几乎不可能的小的建议,就是我打算展示Malloc Maleficarum

      The House of Prime

      The House of Mind

      The House of Force

      The House of Lore

      The House of Spirit

      The House of Chaos

The House of Prime

首先介绍The House of Prime。一个艺术家有一幅画的第一次描边,一个作家总有过写出一首诗的第一行的经历。而对于我来说,在堆溢出发面,则是House of Prime。这是第一次突破,同时也表明了接下来一切都会水到渠成。可以说,这就是拒绝不可能,同时这也是相当困难的。由于这些原因,我觉得有必要将Prime放在Malloc Maleficarum的首要位置,而且,这也是它应得的。

      从纯粹的技术角度出发,House of Prime或许是收集中最没有用的了。当条件允许的时候,使用House of  Mind 或者House of Sprit 几乎可以说总是更好的。为了成功地使用House of Prime技术,我们必须释放两个由自己控制的不同size字段的chunk,然后触发对malloc的调用。

      该技术大致的思想是在某些不可控的情况下(下面讨论),破坏fastbin的最大值,以便于允许我们劫持由malloc调用的时所使用参数的数据结构arena。这样就允许返回任意内存块,或者直接修改可执行的控制数据。

      正如前面所说的,这个技术首先调用free函数释放由我们自己控制的内存。调用free函数会继续调用public_fREe,进而调用内部的函数_int_free。对于House Of Prime来说,public_fREe的细节相对来说不重要。因此我们只需要关注_int_free即可。这是它在glibc-2.3.5中的代码:

void _int_free(mstate av, Void_t* mem)
{
    mchunkptr       p;           /* chunk corresponding to mem */
    INTERNAL_SIZE_T size;        /* its size */
    mfastbinptr*    fb;          /* associated fastbin */
    ...
 
    p = mem2chunk(mem);
    size = chunksize(p);
 
    if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
        || __builtin_expect ((uintptr_t) p & MALLOC_ALIGN_MASK, 0))
    {
        errstr = "free(): invalid pointer";
      errout:
        malloc_printerr (check_action, errstr, mem);
        return;
    
}

  然后就会出现了一个大肆吹嘘的完整性测试。__builtin_expect被构造用于优化,并且不影响结果的正确性。我们必须确保两个测试都失败以便于继续执行。在这个阶段,达到这样的效果并不是很难。

      注意,我们并没有控制p的值。因此可以假设对于非对齐的测试将会失败。另一方面,我们控制了size的值。事实上,这是我们所拥有的最重要的控制,但其范围受到了限制。对于House of Primesize的确切的上界限制并不重要。下界的限制才是这一技术正确执行的关键。

#define SIZE_BITS (PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)
#define chunksize(p)         ((p)->size & ~(SIZE_BITS))


其中PREV_INUSE, IS_MMAPPED 以及 NON_MAIN_ARENA 的定义对应着一个申请的块中size entry里最没有意义的三个比特。Chunksize宏用于清除这三个比特位,这就意味着我们可以控制的chunk最小的值就是8。我们继续看_int_free,然后就清楚为什么这很重要了。

    if ((unsigned long)(size) <= (unsigned long)(av->max_fast))
    {
      if (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
          || __builtin_expect (chunksize (chunk_at_offset (p, size))
                               >= av->system_mem, 0))
        {
          errstr = "free(): invalid next size (fast)";
          goto errout;
        }
 
      set_fastchunks(av);
      fb = &(av->fastbins[fastbin_index(size)]);
 
      if (__builtin_expect (*fb == p, 0))
        {
          errstr = "double free or corruption (fasttop)";
          goto errout;
        }
 
      p->fd = *fb;
      *fb = p;
    
}

这就是fastbin的代码了。至于什么是fastbin以及为什么它们会被使用已经超出了本文的范围了。但是请记住,House of Prime的第一步就是覆盖fastbin的最大sizeav->max_fast。为了达到这样的效果,从上面可以看出,我们必须首先提供具有较低下限大小的块。考虑到av->max_fast的默认值是72,我们就很容易知道fastbin代码主要是对于不大于这样大小的块进行操作。但是,也正是因为这个才会导致av-max_fast的损坏不会立即出现效果。

我们应该注意到avarena的指针,这个东西是一个控制结构,包含了fastbin的最大值,指向fastbins的指针数组。而且,av->max_fastav->fastbins是连续的。

    ...
    INTERNAL_SIZE_T  max_fast;
    mfastbinptr      fastbins[NFASTBINS];
    mchunkptr        top;
    ...

假设下一个大小完整性检验失败了。那么fb就会被设置为&(av->fastbins[fastbin_index(size)]),而其中的size是我们给定的。最后计算出来的下标是由0开始的。然而,下标0对应的chunk大小为最小大小16(一个最小的malloc的块包含prev_sizesize)。因此,当我们提供下限值8的时候,什么会发生呢?我们需要进一步分析fastbin_index()

#define fastbin_index(sz)        ((((unsigned int)(sz)) >> 3) - 2)

我们可以看出,如果原来是8的话,经过宏运算就会得到-1。因此,fb就会被设置为av->fastbins[-1]的地址。由于av->max_fast后续跟着av->fastbins相邻,因此很明显,fb被设置为了&av->max_fast

进一步,由于这时fb不指向p,第二个完整性测试也会失败,最后两行的代码就会被执行。故而,我们的chunk pforward指针被设置为av->max_fastav->max_fast被设置为p的值。

上面我们做了一个假设,那就是下一个大小的完整性检验失败。现实中,我们通常需要做一些工作才可以让它们一起失败。如果溢出可以写空字节,那我们就可以很容易达到目的了。然而,如果溢出以一个空字节结尾,那么我们的解法就会依赖于特定的应用。如果测试是因为溢出时自然的内存布局,那我们就不需要依赖于一些固定的条件了,这就没有问题了。否则,我们就需要做一些内存布局的操作来确保nextsize的值是由我们控制的。

然而,House of Prime的挑战部分并不是去覆盖av->max_fast,而是如何利用覆盖进行任意代码执行。House of Prime通过覆盖特定线程变量arena_key来做到这一点。这就是House of Prime所需要的最难的条件。首先,arena_key仅仅在glibc中的malloc函数使用USE_ARENAS标志(默认使用)被编译的时候才存在。进一步,最重要的是,arena_key的地址一定会比真实的arena地址高。

0xb7f00000 <main_arena>:        0x00000000
0xb7f00004 <main_arena+4>:      0x00000049      <-- max_fast
0xb7f00008 <main_arena+8>:      0x00000000      <-- fastbin[0]
0xb7f0000c <main_arena+12>:     0x00000000      <-- fastbin[1]
....
0xb7f00488 <mp_+40>:            0x0804a000      <-- mp_.sbrk_base
0xb7f0048c <arena_key>:         0xb7f00000

由于arena结构和arena_key来自不同的源文件,这种情况发生与否取决于目标libc是如何编译和链接的。我看到过两种不同的形式,因此,这一点很重要。对于现在,我们假设arena_key在高地址上,因此可以被fastbin 代码覆盖。

      Arena_key是线程特殊的数据,也就意味着每一个执行的线程都有自己独立的arena_key,不受其他线程的影响。因此,在将House of  Prime技术应用到一个线程程序的时候,我们可以考虑一下,否则arena_key可以安全地被作为正常数据去处理。

      Arena_key是一个很有意思的目标,因为它被宏arena_get使用来找到正在执行的线程。也就是说,如果arena_key被某个线程控制,并且areba_get被调用了,那么arena就可以被劫持了。这种类型的arena劫持接下来很快就会讲,但是在讲之前我们必须先来考虑一下实际重写arena_key

      为了覆盖arena_keyfastbin代码又会被利用。这对应着我们第二次调用free来释放我们可以控制大小的块。这在House of Prime的先决条件中已经列出来了。正常情况下,fastbin代码不能够写超过av->fastbin,但是既然av->max_fast在先前已经被破坏了,任何一个大小小于我们第一次使用的块的地址的chunk都会被这个fastbin 代码处理。因此,我们可以写到av->fastbins[fastbin_index(av->max_fast)],这样就有很大的范围来得到arena_key了。

      在上面的例子中,arena_keyav->fastbins[0]0x484个字节。因此我们需要设置相应的索引为1156/sizeof(mfastbinptr),以便于把fb设置为arena_key的地址。假设系统的指针是32位的,那我们就需要设置索引为289。大致计算一下fast_index可以得到

(289 + 2) << 3 = 2328

      这就意味着2328的大小可以导致fb被设置为arena_key。当然,请注意,这个大小仅仅适用于上面我导出来的镜像,每个系统中的相应偏移一般是不一样的。

      现在,如果我们已经破坏了av->max_fast,并且触发了free函数对于大小为2328chunk进行释放,并且假设接下来的nextsize完整性测试失败,那么fb就会被设置为arena_key。同时,我们的第二个chunkforward指针将会被设置为已经存在的arena的地址,并且arena_key将会被设置为我们的第二个chunk的地址。

      当破坏av->max_fast的时候,对于我们来说,只要nextsize的完整性检查被处理了,那控制溢出的块就没有那么重要了。当覆盖arena_key的时候,然而,对于我们来说,控制溢出chunk的部分数据很重要。这是因为溢出的chunk很快就会变成新的arena了。因此,很自然的,至少chunk的部分数据必须被任意控制,否则我们就不能随意控制malloc的结果了。

      调用malloc函数之后会继续调用一个名叫public_malloc的函数。

Void_t* public_mALLOc(size_t bytes)
{
    mstate ar_ptr;
    Void_t *victim;
    ...
    arena_get(ar_ptr, bytes);
    if(!ar_ptr)
      return 0;
    victim = _int_malloc(ar_ptr, bytes);
    ...
    return victim;
}

Arena_get宏通过检索arena_key的线程的特定数据来找到当前的arena,如果失败的话,就会创建一个新的arena。由于arena_key被覆盖为了一个非零的数,因而,我们可以安全地假设arena_get不会尝试创建一个新的arena。在public_malloc中,它会将ar_ptr设置为arena_key的新值,也就是我们所控制的第二个chunk。反过来,这个值和被请求分配的大小一起被传递到内部的_int_malloc函数。

一旦执行到_int_malloc函数,我们有两种方法继续执行。第一种方法是去执行fastbin分配的代码:

Void_t*
_int_malloc(mstate av, size_t bytes)
{
    INTERNAL_SIZE_T nb;               /* normalized request size */
    unsigned int    idx;              /* associated bin index */
    mfastbinptr*    fb;               /* associated fastbin */
    mchunkptr       victim;           /* inspected/selected chunk */
 
    checked_request2size(bytes, nb);
 
    if ((unsigned long)(nb) <= (unsigned long)(av->max_fast)) {
      long int idx = fastbin_index(nb);
      fb = &(av->fastbins[idx]);
      if ( (victim = *fb) != 0) {
        if (fastbin_index (chunksize (victim)) != idx)
          malloc_printerr (check_action, "malloc(): memory"
            " corruption (fast)", chunk2mem (victim));
        *fb = victim->fd;
        check_remalloced_chunk(av, victim, nb);
        return chunk2mem(victim);
      }
    
}

Checked_request2size宏会把请求数据块的大小转换一个内存chunk真正的大小。记住av指向我们所控制的内存,并且这个chunkforward指针已经被fastbin代码破坏了。如果glibcmalloc编译的时候没有添加线程统计(默认选项),那么我们的p->fd对应arenaav->fastbins[0]。为了成功地使用这一攻击技术,av->fastbins[0]必须不能够被使用。这就意味着请求的大小必须大于8.

很有意思的是,如果没有线程统计的话,av->max_fast 对应着p->size。这会强制使得nb小于我们第二个chunk的大小,例子中提供的大小是2328。如果这个成功不了的话,那么我们就需要使用unsorted_chunks或者largebin技术了。

通过在av->fastbins[fastbin_index(nb)]设置一个假的fastbin入口,我们可以使相关代码返回一个实际在栈上的chunk。为了通过victimsize的完整性测试,我们必须让假的fastbin指向一个由我们控制的值。特别地,victim chunk的大小必须和nb有一样的fastbin_index,因此,假的fastbin必须指向我们设置的值的前4个字节处,以便于为chunksize提供正确的位置。

假设栈上有一个我们可以控制的值,应用随后就会处理返回的区域,就好像它在处理一个正常请求返回的内存一样。因此,如果在分配的区域内保存有一个函数返回地址,并且我们可以控制应用向这块区域写什么,那么就可以达到任意代码执行。

我们可以传递一个大于我们第二个chunk大小的参数来触发malloc函数,那使用_int_malloc中的unsorted_chunks代码就可以更好地导致任意内存覆盖。然而,这种技术确需我们我们可以控制第二个chunk更多部分,并且进一步控制目标进程地址空间的两块区域的某处内存。为了触发unsorted_chunk代码,绝对请求大小必须大于512(最大的smallbin chunk size),而且,必须大于假的arenaav->max_fast。假设达到了,我们就可以执行unsorted_chunks的代码了

    for(;;) {
      while ( (victim = unsorted_chunks(av)->bk) !=
unsorted_chunks(av)) {
        bck = victim->bk;
 
        if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
            || __builtin_expect (victim->size > av->system_mem, 0))
          malloc_printerr (check_action, "malloc(): memory"
            " corruption", chunk2mem (victim));
 
        size = chunksize(victim);
 
        if (in_smallbin_range(nb) &&
            bck == unsorted_chunks(av) &&
            victim == av->last_remainder &&
            (unsigned long)(size) > (unsigned long)(nb + MINSIZE)) {
          ...
        }
 
        unsorted_chunks(av)->bk = bck;
        bck->fd = unsorted_chunks(av);
 
        if (size == nb) {
          ...
          return chunk2mem(victim);
        }
        
...

这里有很多事情需要考虑,首先unsorted_chunks宏返回av->bins[0].由于我们可以控制av,所以说我们也控制着unsorted_chunks的结果。这也就意味着victim可以通过建立一个假的指向某块内存的av->bins[0](记为A)被设置为一个任意的地址。反过来A->bk就会包含victim将会被设置到的地址(记为B)。由于victim被设置为一个我们可以控制的任意的地址B,临时的变量bck就可以从B->bk被设置为一个任意的地址了。

为了达到这个技术的目的,B->size必须等于nb。这个要求并不是必要的,但它可以很好的通过两个victimsize完整性检验,同时也可以触发上面所说的条件,具有结束malloc的作用。

由于我们可以将bck设置到一个任意的地址,并且unsorted_chunks返回了我们控制的内存区域A,把bck->fd设置为unsorted_chunks的结果使得我们可以随便设置A的地址空间的任意位置的值。重定向执行只需要设置bck为一个GOT表或者..dtors入口减8的地址即可。这会使得重定向执行到A->prev_size,他可以安全地包含一个jmp,以便于跳过在A->bk处的值。和fastbin 分配代码类似,任意执行地址B被返回给了请求的应用。


原文链接:https://dl.packetstormsecurity.net/papers/attack/MallocMaleficarum.txt

本文由 看雪翻译小组 iromise 翻译

校对: daybreak


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

收藏
免费 1
打赏
分享
最新回复 (2)
雪    币: 6103
活跃值: (1207)
能力值: (RANK:30 )
在线值:
发帖
回帖
粉丝
CCkicker 2017-3-8 10:01
2
0
辛苦辛苦
雪    币: 87152
活跃值: (198939)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
linhanshi 2017-3-9 15:32
3
0
поддержка!
游客
登录 | 注册 方可回帖
返回