死都想不到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()
这个函数可以直接生成)
参考链接: