前言
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 结构体来进行攻击即可。
利用条件:
- 能泄露出
heap
地址和libc
地址 - 能使用一次
largebin attack
- 能控制程序执行
IO
操作,包括但不限于:从main
函数返回、调用exit
函数、通过__malloc_assert
触发 - 能控制
_IO_FILE
的vtable
和_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;
}
总结一下:
- 判断vtable的地址是否处于glibc中的vtable数组段,是的话,通过检查。
- 否则判断是否为外部的合法vtable(重构或是动态链接库中的vtable),是的话,通过检查。
- 否则报错,输出"
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
例题讲解放在下一篇