_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_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/

的第二题。