_IO_FILE与 vtable

_IO_FILE

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中(stdin, stdout, stderr在libc.so的数据段里,并且会在程序开始时被创建)。我们常定义一个指向 FILE 结构的指针来接收这个返回值。

FILE 结构定义在 libio.h 中,关键源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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分别如下:

1
2
3
_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函数会调用的函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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的整合

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

在x86中,file和vtable的偏移是0x94,在x64中的偏移一般是0xd8

(x64下_IO_FILE_plus结构体内的偏移:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);

而_IO_sgetn会调用 _IO_XSGETN

1
2
3
4
5
6
7
8
_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。

检查如下:

1
2
3
4
5
6
7
8
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

1
written = _IO_sputn (fp, (const char *) buf, request);

_IO_sputn会调用 _IO_XSPUTN,指向 _IO_new_file_xsputn,同时也会调用vtable中的 _IO_OVERFLOW。

1
2
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

_IO_OVERFLOW默认指向 _IO_new_file_overflow

1
2
3
4
5
6
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.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
2
3
4
5
6
7
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来辅助了,

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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处的函数

需要满足:

  1. fp->_IO_buf_base != 0
  2. fp->_flags为偶数

这条链是exit来触发的,所以还需要满足_IO_flush_all_lockp的检查:

 1. fp->_IO_write_ptr > fp-> _IO_write_base
 2. fp-> _mode <= 0

所以我们构造起来就非常简单:

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
2
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

所以我们需要构造

1
2
3
4
5
6
7
8
9
_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不需要我们手动调用,在以下情况

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

会自动调用,对_IO_list_all中的每一个文件流使用 _IO_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
2
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有要求的函数就不能这么改。

经典例题就是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中的mallocmemcpy劫持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_assertfflush(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

(待更新)