IO_FILE Summary
_IO_FILE与 vtable
_IO_FILE
FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中(stdin, stdout, stderr在libc.so的数据段里,并且会在程序开始时被创建)。我们常定义一个指向 FILE 结构的指针来接收这个返回值。
FILE 结构定义在 libio.h 中,关键源码如下:
1 | struct _IO_FILE { |
在libc上的stdin, stdout, stderr的symbol分别如下:
1 | _IO_2_1_stdin_ |
所有的_IO_FILE对象由 _chain彼此连接形成了一个单链表,表头是 _IO_list_all。
_IO_list_all初始状态是指向 _IO_2_1_stderr _ 的。
vtable
常规文件流的vtable是一种_IO_jump_t类型的指针,在vtable中保存有许多IO函数会调用的函数指针。
1 | struct _IO_jump_t |
在libc中定义的vtable有_IO_file_jumps
, _IO_str_jumps
, _IO_cookie_jumps
等。
_IO_FILE_plus
_IO_FILE_plus是对 _IO_FILE和vtable的整合
1 | struct _IO_FILE_plus |
在x86中,file和vtable的偏移是0x94,在x64中的偏移一般是0xd8
(x64下_IO_FILE_plus结构体内的偏移:)
1 | 0x0 _flags |
IO函数的调用链
分析
这里先用fread和fwrite为例来分析一下
fread
fread的函数原型:
size_t fread(void *buffer, size_t size, size_t count, FILE *stream)
buffer:存数据的缓冲区
size:指定每次读的数据项长度
count:数据项的个数
stream:目标文件流
返回值:成功读取到缓冲区的数据项个数
fread指向的函数名为_IO_fread,它会调用 _IO_sgetn
1 | bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); |
而_IO_sgetn会调用 _IO_XSGETN
1 | _IO_size_t |
他实际上就是vtable中的函数指针,默认指向_IO_file_xsgetn。
检查如下:
1 | if (fp->_IO_buf_base |
fwrite
fwrite的函数原型:
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
buffer:要写入的地址
size:要写入的每个数据项大小
count:写入多少个数据项
返回值:成功写入的数据项个数
fwrite指向的函数名为_IO_fwrite,它会调用 _IO_sputn
1 | written = _IO_sputn (fp, (const char *) buf, request); |
_IO_sputn会调用 _IO_XSPUTN,指向 _IO_new_file_xsputn,同时也会调用vtable中的 _IO_OVERFLOW。
1 | /* Next flush the (full) buffer. */ |
_IO_OVERFLOW默认指向 _IO_new_file_overflow
1 | if (ch == EOF) |
在_IO_new_file_overflow中进一步调用系统函数write
常见的调用链
- printf/puts -> _IO_file_xsputn
- scanf/gets -> _IO_file_xsgetn
- fwrite -> _IO_new_file_xputn
- fread -> _IO_file_xgetn
- fclose -> _IO_FILE_FINISH
还有经常用于攻击的一个调用链:
- exit -> _IO_flush_all_lockp -> _IO_overflow
攻击手段
伪造vtable
libc-2.23
程序使用fopen打开的文件流是会存在堆上的,位于libc.so数据段的vtable不可写,但是我们可以在可控内存(比如堆)上伪造一个vtable,然后让fp+0xd8指向那个伪造的vtable,在vtable上布置好相应会调用的函数指针从而劫持程序流程。
一个比较典型的例题是HCTF2018 the_end,可以参考我这篇博客的分析
https://kotoriseed.gitee.io/2022/03/31/HCTF2018the_end(exit%20hook)/
原题环境下是libc-2.23,漏洞是任意修改5个byte,由于可写入的内容有限,所以直接在_IO_2_1_stdin _ (或者另外两个也行)附近来伪造vtable就好了(因为这样可以直接改低地址几位,将就现有的值),
2个byte用来修改_IO_2_1_stdin _.vtable,3个byte用来修改 _setbuf指针,使其指向one_gadget的地址。
libc-2.24
很不幸的是,在libc-2.24中,加入了对vtable劫持的检测,会在调用之前检查vtable地址的合法性
1 | uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; |
即,vtable需要在 __start__libc_IO_vtables
与__stop__libc_IO_vtables
之间,不然就会调用 _IO_vtable_check()进行进一步的检查。
所以之前伪造vtable的手段很难实现。
但是_IO_str_jumps
这个vtable是可以通过检查的,我们可以利用它的__IO_str_finish
或者__IO_str_overflow
来达到攻击的目的。
但是老版本的libc中并没有_IO_str_jumps这个symbol,没办法直接定位,我们就需要用到一些相关函数,例如__IO_str_underflow来辅助了,
1 | def get_IO_str_jumps: |
有了__IO_str_jumps之后,就可以选择以下方式来getshell了
_IO_str_jumps -> _IO_str_finish
1 | void _IO_str_finish (FILE *fp, int dummy) |
可以看到,_IO_str_finish以fp->_IO_buf_base
为参数调用了fp+0xE8
处的函数
需要满足:
- fp->_IO_buf_base != 0
- fp->_flags为偶数
这条链是exit来触发的,所以还需要满足_IO_flush_all_lockp的检查:
1. fp->_IO_write_ptr > fp-> _IO_write_base
2. fp-> _mode <= 0
所以我们构造起来就非常简单:
1 | 1. fp->_flag = 0 |
然后将目标文件流的vtable指向_IO_str_jumps-0x8来调用 _IO_str_finish(因为原本要调用的是 _IO_str_overflow,减去0x8即可指向 _IO_str_finish)
_IO_str_jumps -> _IO_str_overflow
1 | v2 = fp->_flags; |
这一条链子就比finish那条分析起来稍微麻烦了一点,他最终是以
2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100
为参数调用fp+0xE0
处的函数。
绕过条件需要满足:
1 | 1). fp->_flags & 8 == 0, (fp-> _flags & 0xC00) == 0x400, fp-> _flags & 1 = 0 |
所以我们需要构造
1 | _flags = 0 |
FSOP
主要利用的就是前面提到过的 __IO_flush_all_lockp -> _IO_overflow这个调用链。
因为__IO_flush_all_lockp不需要我们手动调用,在以下情况
当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时
会自动调用,对_IO_list_all中的每一个文件流使用 _IO_overflow
1 | int |
需要满足的条件也很简单:
1 | 1. fp->_mode <= 0 |
我们一般是将伪造的vtable中的_IO_overflow指向one_gadget或者system。
指向system的时候,因为默认是传自己的fp作为参数,所以需要将fp的_flag那个位置改为’/bin/sh\x00’,要注意的是,有些对 _flag有要求的函数就不能这么改。
经典例题就是house of orange,
可以参考我这个文章:https://kotoriseed.gitee.io/2022/03/22/%E5%AE%89%E6%B4%B5%E6%9D%AF2021/
的第二题。
FSOP的高版本技巧
限于篇幅原因,这里只粗略介绍
libc-2.31
house of pig
适用于存在calloc的题目,用largebin attack
配合tcache stashing unlink attack
将某个hook放入tcache bin的头部,再largebin attack
打_IO_list_all
在堆上伪造好一个_IO_file_plus
,vtable换为_IO_str_jumps
,依赖_IO_str_overflow
中的malloc
和memcpy
劫持hook并getshell。如果开了沙箱,就劫持malloc hook到setcontext+61
,走orw。
house of kiwi
有些题目没有直接调用exit之类的函数,导致fsop不容易触发。house of kiwi利用的是__malloc_assert
中的fflush(stderr)
,他能够稳定调用stderr的虚表(_IO_file_jumps
)的sync。并且在执行sync之前,rdx的值稳定指向_IO_helper_jumps
。所以劫持sync到setcontext+61
,然后改_IO_helper_jumps+0xA0
为orw的rop的地址(劫持rsp),_IO_helper_jumps+0xA8
为一个ret的地址(劫持rcx),即可完成栈迁移orw。
至于__malloc_assert
的触发,通常可以改top chunk为一个较小值(注意prev_inuse bit置0
),然后malloc一个超过top chunk大小的值即可。
libc-2.34
house of emma
由于高版本libc的诸多限制,新的堆利用思路将倾向于向某个可控地址写入某些值而getshell,而非利用任意地址申请的方式getshell。house of emma的思路是用一次largebin attack打libc上的stderr指针(当存在puts
等函数时,也可以劫持stdout
的vtable,那样调的就是_IO_file_xputn
的偏移),伪造好一个_IO_cookie_file
结构体,vtable布置为_IO_cookie_jumps
的某个偏移量(sync的),然后利用__malloc_assert
的fflush(stderr)
来触发_IO_cookie_read
(也可以是_IO_cookie_write
,_IO_cookie_seek
,_IO_cookie_close
),由于_IO_cookie_read
用到了一个函数指针,这个指针在_IO_cookie_file
中可以伪造,所以可以很方便的去控制流程orw或者直接getshell。不过有个 PTR_DEMANGLE (read_cb)
的加密,用到了TLS结构体的__pointer_chk_guard
,所以还需要一次largebin attack来把这个值改为已知堆地址。不过也正是这个原因,无法走exit的流程来fsop,因为exit在调用到我们布置好的fsop之前,还有一个地方要用到PTR_DEMANGLE
这个宏来加密,这个时候就会出问题。
libc-2.35
house of apple
在只有一次任意写堆地址的情况下,house of apple是非常灵活的攻击手法。但是由于依赖_IO_flush_all_lockp
对所有文件流调用_IO_overflow
,所以前提是程序显式调用了exit函数,或能从main函数返回__libc_start_main
。流程如下:
第一次largebin attack打_IO_list_all
,伪造的_IO_FILE的vtable选_IO_wstrn_jumps
,劫持_wide_data
字段到需要任意写已知值的地方。调用_IO_wstrn_overflow
的时候,首先会将该IO_FILE转化为_wstrnfile
,然后将他的overflow_buf
(或附近距离它一定偏移的值)赋值给之前劫持的_wide_data
的_IO_read_base
, _IO_read_ptr
, _IO_read_end
, _IO_write_base
(其实就是目标地址前0x20的值)。overflow_buf
相对于_IO_FILE
结构体的偏移为0xf0。(需要注意的是,在调用_IO_wstrn_overflow
时,会调用_IO_wsetb
,它可能会free掉原IO_FILE的_wide_data->_IO_buf_base
,如果这个值不为0,就会出现异常。但是这个free是可以通过将IO_FILE的_flags2
字段设置为8
来绕过的)。
如此一来,就相当于在只有一次largebin attack的前提下,劫持了_IO_list_all
并额外达到了一次任意地址写已知值,还能利用第一个IO_FILE的_chains
字段去利用其它布置好的IO_FILE来getshell或走orw。
house of cat
(待更新)