之前花了点时间把简单的IO链看了一遍,实际上在题目里面都不是裸的IO链,还需要结合一些堆的常见打法构造任意地址写,任意地址读的机会。而且利用malloc_assert
触发io需要打top_chunk
的size,一般就考虑堆溢出,分配堆块到top_chunk
上,或者是考虑largebin_attack
任意写堆地址的3字节错位把size改成一字节大小。下面对不同版本glibc下的largebin_attack
进行分析
largebin_attack
主要是两个作用,任意地址申请堆块,任意地址写堆地址
其中任意地址申请堆块并不受glibc版本的限制,在2.35下仍然可打,但是初始条件太强,不太好用
任意地址写堆地址在glibc 2.30之前比较容易利用,可以做到两处任意地址写,而在glibc 2.30开始,新增了相关的指针检测,导致原来两处任意地址写不可利用,但是另外一处的链接过程未检测,所以在2.30之后基本就是利用这一处做到一次任意地址写
任意地址申请堆地址 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <stdlib.h> int main () { unsigned long * p1 = malloc (0x410 ); malloc (0x10 ); unsigned long * p2 = malloc (0x420 ); malloc (0x10 ); unsigned long * p3 = malloc (0x420 ); free (p1); free (p2); unsigned long * p4 = malloc (0x600 ); unsigned long bss1 = p3-2 ; p1[3 ] = p3-2 ; p3[0 ] = &bss1-3 ; p3[1 ] = &bss1-2 ; p3[2 ] = 0 ; p3[3 ] = 0 ; (p4-2 )[0 ] = 0x430 ; unsigned long * p5 = malloc (0x420 ); p5[0 ] = 0x2333 ; }
源码
这部分是在largebin中取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 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 if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); if ((victim = first (bin)) != bin && (unsigned long ) chunksize_nomask (victim) >= (unsigned long ) (nb)) { victim = victim->bk_nextsize; while (((unsigned long ) (size = chunksize (victim)) < (unsigned long ) (nb))) victim = victim->bk_nextsize; if (victim != last (bin) && chunksize_nomask (victim) == chunksize_nomask (victim->fd)) victim = victim->fd; remainder_size = size - nb; unlink (av, victim, bck, fwd); if (remainder_size < MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) set_non_main_arena (victim); } else { remainder = chunk_at_offset (victim, nb); bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("malloc(): corrupted unsorted chunks" ); remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; 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; } }
主要关注一个局部
1 2 3 4 5 6 7 8 9 10 11 12 13 while (((unsigned long ) (size = chunksize (victim)) < (unsigned long ) (nb))) victim = victim->bk_nextsize; if (victim != last (bin) && chunksize_nomask (victim) == chunksize_nomask (victim->fd)) victim = victim->fd; remainder_size = size - nb; unlink (av, victim, bck, fwd);
从largebin申请时,先通过malloc大小找到相应index的bin然后从bin的最小的chunk通过bk_nextsize
进行遍历,找到第一个大于等于申请size的chunk进行unlink操作
总共需要做到下面几个步骤
1 2 3 chunksize_nomask (victim) >= (unsigned long ) (nb) victim = victim->bk_nextsize; unlink (av, victim, bck, fwd);
这里借此分析一下unlink,之前一直没搞明白,实际上unlink函数一直没改动,高版本不好打unlink是因为int_free在操作的时候多做了部分检测,导致不好利用unlink的思路来打overlapping
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 static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size" ); mchunkptr fd = p->fd; mchunkptr bk = p->bk; if (__builtin_expect (fd->bk != p || bk->fd != p, 0 )) malloc_printerr ("corrupted double-linked list" ); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL ) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)" ); if (fd->fd_nextsize == NULL ) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } }
从源码上看unlink只需绕几个点
1 2 3 4 if (chunksize (p) != prev_size (next_chunk (p)))__builtin_expect (fd->bk != p || bk->fd != p, 0 ) p->fd_nextsize != NULL
这个手法感觉就是用来打堆重叠的,所以target_addr
一般考虑是heap上的伪造chunk,我们只需要先在largebin里面布置一大一小两chunk,将小chunk的bk_nextsize修改,然后把需要满足的条件改改,最后申请一个介于之间大小的chunk即可完成利用
任意地址写堆地址 Libc<2.30 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> int main () { long target1 = 0 ; long target2 = 0 ; unsigned long * p1 = malloc (0x410 ); malloc (0x10 ); unsigned long * p2 = malloc (0x420 ); malloc (0x10 ); free (p1); malloc (0x500 ); free (p2); p1[1 ] = &target1-2 ; p1[3 ] = &target2-4 malloc (0x500 ); }
源码
向largebin中插入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 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 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 (chunk_main_arena (bck->bk)); 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; 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;
从源码来看我们有以下3个点可以任意地址写堆地址
1 2 3 4 5 6 victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; victim->bk_nextsize = fwd->bk_nextsize; victim->bk_nextsize->fd_nextsize = victim; bck = fwd->bk; bck->fd = victim;
这个地方很容易理解,如果考虑2+3的话就是先在largebin里布置一个小一点的chunk,改chunk的bk,bk_nextsize
为target1-0x10,target2-0x20
,最后再往largebin放入一个大一点的chunk即可完成利用
Libc≥2.30 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> long target1 = 0 ;int main () { unsigned long * p1 = malloc (0x420 ); malloc (0x10 ); unsigned long * p2 = malloc (0x410 ); malloc (0x10 ); free (p1); malloc (0x500 ); free (p2); p1[3 ] = &target1-4 ; malloc (0x500 ); }
源码
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 {{... 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)" ); } } 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;
可以看到这里新加了两处检测,所以低版本的2,3都不再好利用,所以高版本largebin attack都是考虑第一处的任意写
那么的话我们需要做的就是先在largebin中布置一个稍大的chunk,改该chunk的bk_nextsize
为target-0x20
,最后放入小一点的chunk
下面简单说一下house of storm,虽然高版本不再能用,但是这个利用思路确实是比较巧妙的
House of storm House of storm 是largebin attack结合unsorted bin的一种很巧妙的利用方式,由于其需要用到两次任意地址写堆地址,所以仅适用libc<2.30的情况
利用条件如下
在largebin布置一个较小chunk修改bk,bk_nextsize
unsorted bin布置一个大一点的chunk,修改bk
unsorted bin的取出过程
1 2 3 unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
而House of storm就是利用unsorted bin中的chunk在放入largebin中时利用largebin attack伪造一个chunk的size(0x56)以及bk,而在unsorted bin的解链过程中会把这个fake chunk链入unsorted_chunks (av)
,从而在申请0x50大小堆块时就做到了分配堆块到target_addr
的目的