死都想不到house of apple居然有三篇文章,这里就把第二篇和第三篇合并起来一起学完吧。
house of apple2
利用条件:
- 已知 - heap地址和- glibc地址
- 能控制程序执行 - IO操作,包括但不限于:从- main函数返回、调用- exit函数、通过- __malloc_assert触发
- 能控制 - _IO_FILE的- vtable和- _wide_data,一般使用- largebin attack去控制
利用原理
在https://cv196082.gitee.io/2022/02/23/FSOP/ 这篇文章中详细介绍了去调用vtable中的函数指针时会经过什么验证。
| 1 | struct _IO_wide_data | 
注意_IO_wide_data结构体中存在_wide_vtable成员,在调用_wide_vtable虚表是同样会经过一系列宏去调用:
| 1 | 
在这里调用的顺序中没有出现vtable是否合法的检测,因此我们可以劫持_IO_FILE的vtable为_IO_wfile_jumps,控制_wide_data为可以控制的堆空间,进一步控制_wide_data->_wide_vtable指向可以控制的堆地址,控制程序IO流函数调用,最终调用到IO_wxxxxx
利用思路
这里最终目的是调用_wide_vtable成员,所以需要找到上述宏的调用,最终发现只存在以下四个:_IO_WSETBUF、_IO_WUNDERFLOW、_IO_WDOALLOCATE、_IO_WOVERFLOW
并且其中前两个还是利用难度较高,甚至是无法利用。下面从原文的三个方向出发
_IO_wfile_overflow
这里的调用链为:_IO_wfile_overflow=>_IO_wdoallocbuf=>_IO_WDOALLOCATE=>*(fp->_wide_data->_wide_vtable+ 0x68)(fp)
| 1 | wint_t | 
需要满足f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0和f->_wide_data->_IO_write_base == 0
接着看_IO_wdoallocbuf
| 1 | void | 
这里就调用了IO_Wxxxx并且需要满足fp->_wide_data->_IO_buf_base != 0和fp->_flags & _IO_UNBUFFERED == 0。
所以总的来说构造方式为:
- _flags = ~(2 | 0x8 | 0x800)即可,所以可以直接设置为0或者这值为sh;
- vtable设置为- _IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用- _IO_wfile_overflow即可
- _wide_data设置为可控堆地址- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_write_base设置为- 0,即满足- *(A + 0x18) = 0
- _wide_data->_IO_buf_base设置为- 0,即满足- *(A + 0x30) = 0
- _wide_data->_wide_vtable设置为可控堆地址- B,即满足- *(A + 0xe0) = B
- _wide_data->_wide_vtable->doallocate设置为地址- C用于劫持- RIP,即满足- *(B + 0x68) = C
_IO_wfile_underflow_mmap
调用链:_IO_wfile_underflow_mmap=>_IO_wdoallocbuf=>_IO_WDOALLOCATE=>*(fp->_wide_data->_wide_vtable +0x68)(fp)
| 1 | static wint_t | 
这里需要改写的有点略多了,需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base == NULL和fp->_wide_data->_IO_save_base == NULL。
构造方式:
- _flags设置为- ~4,如果不需要控制- rdi,设置为- 0即可;如果需要获得- shell,可设置为- sh;
- vtable设置为- _IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用- _IO_wfile_underflow_mmap即可
- _IO_read_ptr < _IO_read_end,即满足- *(fp + 8) < *(fp + 0x10)
- _wide_data设置为可控堆地址- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足- *A >= *(A + 8)
- _wide_data->_IO_buf_base设置为- 0,即满足- *(A + 0x30) = 0
- _wide_data->_IO_save_base设置为- 0或者合法的可被- free的地址,即满足- *(A + 0x40) = 0
- _wide_data->_wide_vtable设置为可控堆地址- B,即满足- *(A + 0xe0) = B
- _wide_data->_wide_vtable->doallocate设置为地址- C用于劫持- RIP,即满足- *(B + 0x68) = C
_IO_wdefault_xsgetn
调用链:_IO_wdefault_xsgetn=>__wunderflow=>_IO_switch_to_wget_mode=>_IO_WOVERFLOW=>*(fp->_wide_data->_wide_vtable+0x18)(fp)
| 1 | size_t | 
需要设置fp->_wide_data->_IO_read_ptr == fp->_wide_data->_IO_read_end,使得count为0,不进入if分支。
| 1 | wint_t | 
需要设置fp->mode > 0,并且fp->_flags & _IO_CURRENTLY_PUTTING != 0。
| 1 | int | 
需要设置fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
构造方式:
- _flags设置为- 0x800
- vtable设置为- _IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用- _IO_wdefault_xsgetn即可
- _mode设置为大于- 0,即满足- *(fp + 0xc0) > 0
- _wide_data设置为可控堆地址- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_read_end == _wide_data->_IO_read_ptr设置为- 0,即满足- *(A + 8) = *A
- _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足- *(A + 0x20) > *(A + 0x18)
- _wide_data->_wide_vtable设置为可控堆地址- B,即满足- *(A + 0xe0) = B
- _wide_data->_wide_vtable->overflow设置为地址- C用于劫持- RIP,即满足- *(B + 0x18) = C
house of apple3
前两篇文章中的利用链主要关注_wide_data成员,而本篇文章并不会特别关注_wide_data,而是关注FILE结构体的另外一个成员_codecvt的利用。
利用条件
- 已知heap地址和glibc地址
- 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
- 能控制_IO_FILE的vtable和_codecvt,一般使用largebin attack去控制
注意
上面提到,本篇文章并不会特别关注_wide_data成员,这是因为_wide_data设置不当的话会影响某些利用链的分支走向。但是,如果采用默认的_wide_data成员(默认会指向_IO_wide_data_2,除了_wide_vtable外其他成员均默认为0),也并不影响house of apple3的利用。因此,如果能伪造整个FILE结构体,则需要设置合适的_wide_data;如果只能伪部分FILE的成员的话,保持fp->_wide_data为默认地址即可。
利用原理
FILE结构体中有一个成员struct _IO_codecvt *_codecvt;,偏移为0x98。
| 1 | struct _IO_codecvt | 
__cd_in和__cd_out是同一种类型的数据。
| 1 | typedef struct | 
再观察以下结构体俩变量的定义:
| 1 | struct __gconv_step | 
| 1 | struct __gconv_step_data | 
house of apple3的利用主要关注以下三个函数:__libio_codecvt_out、__libio_codecvt_in和__libio_codecvt_length。三个函数的利用点都差不多
| 1 | enum __codecvt_result | 
| 1 | typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *, | 
这里呢最后这个宏就是调用fct (gs, …)
在_IO_wfile_underflow函数中调用了__libio_codecvt_in
| 1 | wint_t | 
_IO_wfile_underflow又是_IO_wfile_jumps这个_IO_jump_t类型变量的成员函数
所以总的来说利用方式为:劫持或者伪造FILE结构体的fp->vtable为_IO_wfile_jumps,fp->_codecvt为可控堆地址,当程序执行IO操作时,控制程序执行流走到_IO_wfile_underflow,设置好fp->codecvt->__cd_in结构体,使得最终调用到__libio_codecvt_in中的DL_CALL_FCT宏,伪造函数指针,进而控制程序执行流。需要注意的是设置gs->__shlib_handle == NULL绕过__pointer_guard指针的加密保护
利用思路
因为原文作者确实非常牛逼,甚至链都找好了,所以这里还是贴上原文的内容
_IO_wfile_underflow
调用链:_IO_wfile_underflow=>__libio_codecvt_in=>DL_CALL_FCT=>gs =fp->_codecvt->__cd_in.step=>*(gs->__fct)(gs)
这条链是在利用原理当作示例的一条,所以这里只给出构造方式:
- _flags设置为- ~(4 | 0x10)
- vtable设置为- _IO_wfile_jumps地址(加减偏移),使其能成功调用- _IO_wfile_underflow即可
- fp->_IO_read_ptr < fp->_IO_read_end,即满足- *(fp + 8) < *(fp + 0x10)
- _wide_data保持默认,或者设置为堆地址,假设其地址为- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足- *A >= *(A + 8)
- _codecvt设置为可控堆地址- B,即满足- *(fp + 0x98) = B
- codecvt->__cd_in.step设置为可控堆地址- C,即满足- *B = C
- codecvt->__cd_in.step->__shlib_handle设置为- 0,即满足- *C = 0
- codecvt->__cd_in.step->__fct设置为地址- D,地址- D用于控制- rip,即满足- *(C + 0x28) = D。当调用到- D的时候,此时的- rdi为- C。如果- _wide_data也可控的话,- rsi也能控制。
_IO_wfile_underflow_mmap
调用链:_IO_wfile_underflow_mmap=>__libio_codecvt_in=>DL_CALL_FCT=>gs=fp->_codecvt->__cd_in.step=>*(gs->__fct)(gs)
| 1 | static wint_t | 
需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base != NULL不进入调用。
构造方法:
- _flags设置为- ~4
- vtable设置为- _IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用- _IO_wfile_underflow_mmap即可
- _IO_read_ptr < _IO_read_end,即满足- *(fp + 8) < *(fp + 0x10)
- _wide_data保持默认,或者设置为堆地址,假设其地址为- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足- *A >= *(A + 8)
- _wide_data->_IO_buf_base设置为非- 0,即满足- *(A + 0x30) != 0
- _codecvt设置为可控堆地址- B,即满足- *(fp + 0x98) = B
- codecvt->__cd_in.step设置为可控堆地址- C,即满足- *B = C
- codecvt->__cd_in.step->__shlib_handle设置为- 0,即满足- *C = 0
- codecvt->__cd_in.step->__fct设置为地址- D,地址- D用于控制- rip,即满足- *(C + 0x28) = D。当调用到- D的时候,此时的- rdi为- C。如果- _wide_data也可控的话,- rsi也能控制。
_IO_wdo_write
调用链:_IO_new_file_sync=>_IO_do_flush=>_IO_wdo_write=>__libio_codecvt_out=>DL_CALL_FCT=>gs = fp->_codecvt->__cd_out.step=>*(gs->__fct)(gs)
| 1 | int | 
满足fp->_IO_write_ptr > fp->_IO_write_base。
| 1 | 
使fp->_mode > 0。
| 1 | int | 
满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base,然后这个判断需要为假fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base。
构造方式:
- vtable设置为- _IO_file_jumps地址(加减偏移),使其能成功调用- _IO_new_file_sync即可
- _IO_write_ptr > _IO_write_base,即满足- *(fp + 0x28) > *(fp + 0x20)
- _mode > 0,即满足- (fp + 0xc0) > 0
- _IO_write_end != _IO_write_ptr或者- _IO_write_end == _IO_write_base,即满足- *(fp + 0x30) != *(fp + 0x28)或者- *(fp + 0x30) == *(fp + 0x20)
- _wide_data设置为堆地址,假设地址为- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_write_ptr >= _wide_data->_IO_write_base,即满足- *(A + 0x20) >= *(A + 0x18)
- _codecvt设置为可控堆地址- B,即满足- *(fp + 0x98) = B
- codecvt->__cd_out.step设置为可控堆地址- C,即满足- *(B + 0x38) = C
- codecvt->__cd_out.step->__shlib_handle设置为- 0,即满足- *C = 0
- codecvt->__cd_out.step->__fct设置为地址- D,地址- D用于控制- rip,即满足- *(C + 0x28) = D。当调用到- D的时候,此时的- rdi为- C。如果- _wide_data也可控的话,- rsi也能控制。
_IO_wfile_sync
调用链:_IO_wfile_sync=>__libio_codecvt_length=>DL_CALL_FCT=>gs = fp->_codecvt->__cd_in.step=>*(gs->__fct)(gs)
| 1 | wint_t | 
设置fp->_wide_data->_IO_write_ptr <= fp->_wide_data->_IO_write_base和fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0。可以看到这里还需要绕过__libio_codecvt_encoding函数
| 1 | int | 
设置fp->codecvt->__cd_in.step->__stateful != 0即可返回-1。
构造方式:
- _flags设置为- ~(4 | 0x10)
- vtable设置为- _IO_wfile_jumps地址(加减偏移),使其能成功调用- _IO_wfile_sync即可
- _wide_data设置为堆地址,假设其地址为- A,即满足- *(fp + 0xa0) = A
- _wide_data->_IO_write_ptr <= _wide_data->_IO_write_base,即满足- *(A + 0x20) <= *(A + 0x18)
- _wide_data->_IO_read_ptr != _wide_data->_IO_read_end,即满足- *A != *(A + 8)
- _codecvt设置为可控堆地址- B,即满足- *(fp + 0x98) = B
- codecvt->__cd_in.step设置为可控堆地址- C,即满足- *B = C
- codecvt->__cd_in.step->__stateful设置为非- 0,即满足- *(B + 0x58) != 0
- codecvt->__cd_in.step->__shlib_handle设置为- 0,即满足- *C = 0
- codecvt->__cd_in.step->__fct设置为地址- D,地址- D用于控制- rip,即满足- *(C + 0x28) = D。当调用到- D的时候,此时的- rdi为- C。如果- rsi为- &codecvt->__cd_in.step_data可控。
总结
house of apple1主要作用就是实现任意地址写,而我认为house of apple2是最吊的,一定程度上减少了覆盖这个SB pointer_guard。而house of apple3在构造方面要求相对来说较为苛刻。(我还跟个SB一样准备自己写pack_file函数来构造IO_FILE结构体,但是我发现pwntools居然有FileStructure()这个函数可以直接生成)
参考链接: