浅析house_of_apple2(上)

前言

house_of_apple一共分成三个,分别为house_of_apple1,house_of_apple2,house_of_apple。个人认为house_of_apple2相对简单一些,这里先介绍我个人对于house_of_apple2的理解。

注:本文glibc源码版本为2.35

house_of_apple2

原理:

stdin/stdout/stderr这三个_IO_FILE结构体使用的是_IO_file_jumps这个vtable,而当需要调用到vtable里面的函数指针时,会使用宏去调用。那么我们只需要设计好 伪造的FILE 结构体来进行攻击即可。

利用条件:

  1. 能泄露出heap地址和libc地址
  2. 能使用一次largebin attack
  3. 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
  4. 能控制_IO_FILEvtable_wide_data,一般使用largebin attack去控制。

绕过检查: 

1.vtable check:

   glibc 2.24引入了vtable check:

IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /*快速路径:vtable 指针位于 __libc_IO_vtables 部分内。*/
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /*vtable 指针不在预期的部分中。 使用慢速路径,如有必要,该路径将终止进程。*/
    _IO_vtable_check ();
  return vtable;
}

就是说glibc中是有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址:往常覆盖vtable到堆栈上的方式无法绕过此检查,会进入到_IO_vtable_check检查中,函数定义在/libio/vtables.c中:

_IO_vtable_check (void)
{
#ifdef SHARED
  /*遵守兼容性标志。*/
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

  /*如果这个 libc 副本位于非默认命名空间中,我们始终需要接受外部 vtables,因为始终存在 FILE * 对象跨链接边界传递的可能性。*/
  {
    Dl_info di;
    struct link_map *l;
    if (!rtld_active ()
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }

#else /*!共享*/
  /*在静态 dlopen 情况下,我们无法执行 vtable 验证,因为 FILE * 句柄可能会在边界之间来回传递。 因此,我们在这种情况下禁用检查。 */
  if (__dlopen != NULL)
    return;
#endif

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

进入该函数意味着目前的vtable不是glibc中的vtable,因此_IO_vtable_check判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。

看一下这个判断:

if (!rtld_active ()
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))

先看rtld_active()函数:

rtld_active (void)
{
  /*默认初始化变量没有非零dl_init_all_dirs成员,因此这使我们能够识别已初始化且活动的 ld.so 副本。*/
  return GLRO(dl_init_all_dirs) != NULL;
}

不难发现就是在检查dl_init_all_dirs是否非空,非空返回1,空返回0。 

 再看_dl_addr()函数:

int
_dl_addr (const void *address, Dl_info *info,
	  struct link_map **mapp, const ElfW(Sym) **symbolp)
{
  const ElfW(Addr) addr = DL_LOOKUP_ADDRESS (address);
  int result = 0;

  /*防止并发负载和卸载。*/
  __rtld_lock_lock_recursive (GL(dl_load_lock));

  struct link_map *l = _dl_find_dso_for_object (addr);

  if (l)
    {
      determine_info (addr, l, info, mapp, symbolp);
      result = 1;
    }

  __rtld_lock_unlock_recursive (GL(dl_load_lock));

  return result;
}

总结一下:

  1. 判断vtable的地址是否处于glibc中的vtable数组段,是的话,通过检查。
  2. 否则判断是否为外部的合法vtable(重构或是动态链接库中的vtable),是的话,通过检查。
  3. 否则报错,输出"Fatal error: glibc detected an invalid stdio handle,"程序退出。

 2.怎么绕过?

很显然,绕过方式我们只能去考虑怎么去绕过_IO_vtable_check函数的检查,根据_IO_vtable_check的检查机制,我们有两个方法:

1.flag == &_IO_vtable_check

2.rtld_active ()==0
   或者(_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)

 第一种方式不可控,因为flag的检查机制类似于canary,是从栈上取数据的,我们难以控制。

第二种办法好操作的只有rtld_active ()==0,就是去篡改dl_init_all_dirs,但是我们都能篡改dl_init_all_dirs,那我们在低版本下就可以去篡改hook了,这对house_of_apple2的通杀性有影响。

所以提出了先使用内部的vtable数组内的_IO_wfile_jumps、_IO_wfile_jumps_mmap、_IO_wfile_jumps_maybe_mmap等来对vtable进行篡改,从而可以调用_IO_wfile_overflow,从而控制_IO_wide_data为可控的堆地址空间,进而控制_IO_wide_data->_wide_vtable为可控的堆地址空间,来进行利用。

具体原理分析:

这里结合源码来进行分析。

执行exit的流程:

这里先给一条调用链:

exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_overflow -> _IO_wdoallocbuf -> target

程序在调用 __run_exit_handlers函数后,经过一系列操作后(在遍历完exit_funcs链表后)会去调用_libc_atexit,这其实是libc中的一个段

 

 其中默认只有一个函数fcloseall(),这个函数会调用_IO_cleanup():

int
__fcloseall (void)
{
  /* Close all streams.  */
  return _IO_cleanup ();
}

之后就是调用_IO_flush_all_lockp:

int
_IO_cleanup (void)
{
  /* We do *not* want locking.  Some threads might use streams but
     that is their problem, we flush them underneath them.  */
  int result = _IO_flush_all_lockp (0);

  /* We currently don't have a reliable mechanism for making sure that
     C++ static destructors are executed in the correct order.
     So it is possible that other static destructors might want to
     write to cout - and they're supposed to be able to do so.

     The following will make the standard streambufs be unbuffered,
     which forces any output from late destructors to be written out. */
  _IO_unbuffer_all ();

  return result;
}

这里我们只重点看一下_IO_flush_all_lockp函数:

_IO_flush_all_lockp
源码:
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((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))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

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

#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif

  return result;
}

当然这里我们也能验证普通的调用overflow会对vtable进行检查。


  


我们都知道程序在进入exit会调用_IO_jumps_t结构体中的(_IO_overflow_t)类型的函数来进行清空缓冲区:

struct _IO_FILE_plus
{
  FILE file;
  const struct _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);
};

_IO_jump_t内的宏定义:

那么如果我们将原本的vtable换成同样是_IO_jump_t类型的_IO_wfile_jumps会怎么样?

在详细分析前,先认识两个结构体_IO_wfile_jumps和_IO_wide_data:

_IO_wfile_jumps:

const struct _IO_jump_t _IO_wfile_jumps =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

_IO_wide_data:

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

我们知道程序调用overflow函数的时候使用了宏定义,我们展开来看一下:

宏定义:

粗略分析一下:

#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

一步一步看到最后我们发现他在调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查。就是说我们可以控制这条IO调用流(最终调用到_IO_Wxxxxx函数即可控制程序的执行流)。

也就是说我们先通过篡改IO结构体里面的vtable为_IO_wfile_jumps以便绕过vtable check的检查

再通过其去调用属于_overflow处的 _IO_wfile_overflow 达到后续控制IO流的效果。


控制执行流:

roderick01 师傅在文章里一共给出了三条可行的能执行到 _IO_Wxxxxx 函数的利用,包括了:

  • _IO_wfile_overflow
  • _IO_wfile_underflow_mmap
  • _IO_wdefault_xsgetn

这里只分析第一种:

_IO_wfile_overflow

源码分析:
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /*如果当前正在读取或未分配缓冲区。*/
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);
	  _IO_free_wbackup_area (f);
	  _IO_wsetg (f, f->_wide_data->_IO_buf_base,
		     f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

	  if (f->_IO_write_base == NULL)
	    {
	      _IO_doallocbuf (f);
	      _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
	    }
	}
      else
	{
	  /*否则必须正在阅读。 如果 _IO_read_ptr(因此也_IO_read_end)位于缓冲区端,则在逻辑上将缓冲区向前滑动一个块(通过将读取指针设置为块开头的所有点)。 这为后续输出腾出了空间。否则,请将读取指针设置为 _IO_read_end(不理会该指针,以便它可以继续对应于外部位置)。*/
	  if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
	    {
	      f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
	      f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr =
		f->_wide_data->_IO_buf_base;
	    }
	}
      f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
      f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
      f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
      f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr =
	f->_wide_data->_IO_read_end;

      f->_IO_write_ptr = f->_IO_read_ptr;
      f->_IO_write_base = f->_IO_write_ptr;
      f->_IO_write_end = f->_IO_buf_end;
      f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

      f->_flags |= _IO_CURRENTLY_PUTTING;
      if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
	f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
    }
  if (wch == WEOF)
    return _IO_do_flush (f);
  if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
    /*缓冲区真的很满*/
    if (_IO_do_flush (f) == EOF)
      return WEOF;
  *f->_wide_data->_IO_write_ptr++ = wch;
  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
    if (_IO_do_flush (f) == EOF)
      return WEOF;
  return wch;
}

其中只有进行到_IO_wdoallocbuf才能到达我们的目标 ,所以我们需要绕过三个检查:

1.if (f->_flags & _IO_NO_WRITES) 

2. if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)

3.if (f->_wide_data->_IO_write_base == 0)

其中

#define _IO_NO_WRITES 0x0008
#define _IO_CURRENTLY_PUTTING 0x0800

也就是说我们需要绕过:

1. f->flags!=0x8  &&  f->flags!=0x800

2.f->_wide_data->_IO_write_base == 0

接着进入_IO_wdoallocbuf函数

_IO_wdoallocbuf
源码:
void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
		     fp->_wide_data->_shortbuf + 1, 0);
}

其中我们希望他进入_IO_WDOALLOCATE这个宏操作,这里我们有两个地方需要绕过:

1.fp->_wide_data->_IO_buf_base == 0
2.fp->_flags & _IO_UNBUFFERED(0x2) == 0 

在来看看_IO_WDOALLOCATE

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

 继续扩展宏操作:

#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

 手动扩展一下:

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
(wint_t)_IO_WDOALLOCATE (fp)
-->>WJUMP0 (__doallocate, fp)
    #define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
-->>(_IO_WIDE_JUMPS_FUNC(fp)->__doallocate) (fp)   
    #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
-->>(_IO_WIDE_JUMPS(fp)->__doallocate) (fp)  
    #define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
-->>....
一堆破宏操作
-->>((*(__typeof__ (((struct _IO_FILE){})._wide_data) *)(((char *) ((fp))) + __builtin_offsetof (struct _IO_FILE, _wide_data)))->_wide_vtable->__doallocate) (fp)

(__doallocate这个请看_IO_jump_t结构体,这是其中的一个项,最后的结果来自vscode的自动扩展功能)


我们再简化一下最后的结果:

((*(__typeof__ (((struct _IO_FILE){})._wide_data) *)(((char *) ((fp))) + __builtin_offsetof (struct _IO_FILE, _wide_data)))->_wide_vtable->__doallocate) (fp)

-->>*(fp->_wide_data->_wide_vtable + 0x68)(fp)

总结一下函数调用链:

_IO_wfile_overflow
-->>_IO_wdoallocbuf
    -->>_IO_WDOALLOCATE
        -->>*(fp->_wide_data->_wide_vtable + 0x68)(fp)/
            *(fp->_wide_data->_wide_vtable->_doallocate)(fp)

就是说最后我们能够call *(fp->_wide_data->_wide_vtable + 0x68)(fp)

总结:

我们利用的流程:

伪造IO结构体的vtable段为_IO_wfile_jumps,以绕过vtable check的检查,之后顺势跳转到 _IO_wfile_overflow函数,经过一系列函数和宏操作之后便call   *(fp->_wide_data->_wide_vtable + 0x68)(fp),其中fp就是指向我们伪造的IO结构体的指针。这里给一张图方便理解。

    需要绕过的检查(偏移可以去看我的另一篇文章:浅析house_of_orange-CSDN博客):

  • f->flags!=0x8  &&  f->flags!=0x800 && f->flags!=0x2
  • vtable设置为_IO_wfile_jumps使其能成功调用_IO_wfile_overflow即可
  • _wide_data设置为可控堆地址heap_addr1,即满足*(f + 0xa0) = heap_addr1
  • _wide_data->_IO_write_base设置为0,即满足*(heap_addr1 + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(heap_addr1 + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址heap_addr2,即满足*(heap_addr1 + 0xe0) = heap_addr2
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持执行流,即满足*(heap_addr2 + 0x68) = C

 例题讲解放在下一篇

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值