IO_FILE Summary
_IO_FILE与 vtable
_IO_FILE
FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中(stdin, stdout, stderr在libc.so的数据段里,并且会在程序开始时被创建)。我们常定义一个指向 FILE 结构的指针来接收这个返回值。
FILE 结构定义在 libio.h 中,关键源码如下:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
在libc上的stdin, stdout, stderr的symbol分别如下:
_IO_2_1_stdin_
_IO_2_1_stdout_
_IO_2_1_stderr_
所有的_IO_FILE
对象由 _chain
彼此连接形成了一个单链表,表头指针是 _IO_list_all
。
_IO_list_all
初始状态是指向 _IO_2_1_stderr_
的。
vtable
常规文件流的vtable是一种_IO_jump_t
类型的指针,在vtable中保存有许多IO函数会调用的函数指针。
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);
#if 0
get_column;
set_column;
#endif
};
在libc中定义的vtable有_IO_file_jumps
, _IO_str_jumps
, _IO_cookie_jumps
等。
_IO_FILE_plus
_IO_FILE_plus是对 _IO_FILE和vtable的整合
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
在x86中,file和vtable的偏移是0x94,在x64中的偏移一般是0xd8
(x64下_IO_FILE_plus结构体内的偏移:)
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable
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
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
而_IO_sgetn会调用 _IO_XSGETN
_IO_size_t
_IO_sgetn (fp, data, n)
_IO_FILE *fp;
void *data;
_IO_size_t n;
{
return _IO_XSGETN (fp, data, n);
}
他实际上就是vtable中的函数指针,默认指向_IO_file_xsgetn。
检查如下:
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
fwrite
fwrite的函数原型:
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
buffer:要写入的地址
size:要写入的每个数据项大小
count:写入多少个数据项
返回值:成功写入的数据项个数
fwrite指向的函数名为_IO_fwrite,它会调用 _IO_sputn
written = _IO_sputn (fp, (const char *) buf, request);
_IO_sputn会调用 _IO_XSPUTN,指向 _IO_new_file_xsputn,同时也会调用vtable中的 _IO_OVERFLOW。
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
_IO_OVERFLOW默认指向 _IO_new_file_overflow
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return 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.github.io/post/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地址的合法性
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))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
即,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来辅助了,
def get_IO_str_jumps:
IO_file_jumps = libcbase + libc.symbols['_IO_file_jumps']
IO_str_underflow_offset = libc.symbols['_IO_str_underflow']
for temp in libc.search(p64(IO_str_underflow_offset)):
IO_str_jumps = libcbase + (temp - 0x20) #_IO_str_underflow-0x20就是_IO_str_jumps
if IO_str_jumps > IO_file_jumps:
return IO_str_jumps
有了__IO_str_jumps之后,就可以选择以下方式来getshell了
_IO_str_jumps -> _IO_str_finish
void _IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & 1))
((void (*)(void))fp + 0xE8 ) (fp->_IO_buf_base); // call qword ptr [fp+E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
可以看到,_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. fp->_flag = 0
2. fp->_IO_write_base = 0
3. fp->_IO_write_ptr = 1
4. fp->_IO_buf_base = str_binsh_addr
5. fp->_mode = 0
6. fp+0xE8 = system_addr
然后将目标文件流的vtable指向_IO_str_jumps-0x8来调用 _IO_str_finish(因为原本要调用的是 _IO_str_overflow,减去0x8即可指向 _IO_str_finish)
_IO_str_jumps -> _IO_str_overflow
v2 = fp->_flags;
if ( fp->_flags & 8 )
return (unsigned int)-(a2 != -1);
if ( (fp->_flags & 0xC00) == 0x400 )
{
v4 = fp->_IO_read_ptr;
v11 = fp->_IO_read_end;
BYTE1(v2) |= 8u;
LODWORD(fp->_flags) = v2;
fp->_IO_write_ptr = v4;
fp->_IO_read_ptr = v11;
}
else
{
v4 = fp->_IO_write_ptr;
}
v6 = (char *)fp->_IO_buf_end - (char *)fp->_IO_buf_base
if ( (char *)v4 - (char *)fp->_IO_write_base >= v6 + (a2 == -1) )
{
if ( v2 & 1 )
return 0xFFFFFFFFLL;
v7 = 2 * v6 + 100;
if ( v6 > v7 )
return 0xFFFFFFFFLL;
v8 = ((__int64 (__fastcall *)(unsigned __int64)) fp + 0xE0)(2 * v6 + 100); // call
v9 = v8;
.........
}
这一条链子就比finish那条分析起来稍微麻烦了一点,他最终是以
2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100
为参数调用fp+0xE0
处的函数。
绕过条件需要满足:
1). fp->_flags & 8 == 0, (fp-> _flags & 0xC00) == 0x400, fp-> _flags & 1 = 0
2). fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base
所以我们需要构造
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_base = 0
_IO_buf_end = (binsh_in_libc_addr -100) / 2
_mode = -1
vtable = _IO_str_jumps - 0x18
FSOP
主要利用的就是前面提到过的 __IO_flush_all_lockp -> _IO_overflow这个调用链。
因为__IO_flush_all_lockp不需要我们手动调用,在以下情况
-
当 libc 执行 abort 流程时
-
当执行 exit 函数时
-
当执行流从 main 函数返回时
会自动调用_IO_list_all
中的每一个文件流的_IO_overflow
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}
需要满足的条件也很简单:
1. fp->_mode <= 0
2. fp->_IO_write_ptr > fp->_IO_write_base
我们一般是将伪造的vtable中的_IO_overflow指向one_gadget或者system。
指向system的时候,因为默认是传自己的fp作为参数,所以需要将fp的_flag那个位置改为'/bin/sh\x00',要注意的是,有些对 _flag有要求的函数就不能这么改。
(若没法使用fsop触发利用链,可以参考house of kiwi
,走__malloc_assert
)
经典例题就是house of orange
,
参考例题:2021安洵杯-ezheap
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。
参考例题:2022强网杯-house of cat