IO_FILE相关笔记

IO_FILE相关笔记


一、IO_FILE相关结构

_IO_FILE_plus_IO_FILE_IO_jump_t结构体的定义分别为:

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
67
68
69
70
71
72
73
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
--------------------------------------------------------------------------------------------
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_gtr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
--------------------------------------------------------------------------------------------
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
};


进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps

image-20250420202923575

此外,还有_IO_wide_data结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct _IO_wide_data
{
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
[...]
const struct _IO_jump_t *_wide_vtable;
};

宏的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000
#define _OLD_STDIO_MAGIC 0xFABC0000
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4
#define _IO_NO_WRITES 8
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40
#define _IO_LINKED 0x80
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

IO 调用的 vtable 函数:

fread 函数中调用的 vtable 函数有:

_IO_sgetn 函数调用了 vtable 的 _IO_file_xsgetn
_IO_doallocbuf 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。
vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。
__underflow 函数调用了 vtable 中的 _IO_new_file_underflow实现文件数据读取。 vtable 中的 _IO_new_file_underflow调用了vtable__GI__IO_file_read`最终去执行系统调read。

fwrite 函数调用的 vtable 函数有:

_IO_fwrite 函数调用了 vtable 的 _IO_new_file_xsputn
_IO_new_file_xsputn 函数调用了 vtable 中的 _IO_new_file_overflow 实现缓冲区的建立以及刷新缓冲区。
vtable 中的 _IO_new_file_overflow 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。
vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。
new_do_write 中的 _IO_SYSWRITE调用了 vtable_IO_new_file_write 最终去执行系统调用write。

fclose 函数调用的 vtable 函数有:

在清空缓冲区的 _IO_do_write 函数中会调用 vtable 中的函数。
关闭文件描述符 _IO_SYSCLOSE 函数为 vtable 中的 __close 函数。
_IO_FINISH 函数为 vtable 中的 __finish 函数。

正常情况下exit函数调用的函数链:

exit() –> __run_exit_handlers() –> exit_function_list -> _dl_fini() –> __dl_rtld_lock_recursive__dl_rtld_unlock_recursive

IO_validate_vtable 函数的调用链:(在调用vtable前执行,且rtld_active为true时)

IO_validate_vtable –> rtld_active –> _dl_addr –> __rtld_lock_lock_recursive (GL(dl_load_lock))

二、IO_FILE attack 之 FSOP (libc 2.23 & 2.24)

主要原理为劫持vtable_chain,伪造IO_FILE,主要利用方式为调用**IO_flush_all_lockp()**函数触发。
IO_flush_all_lockp()函数将在以下三种情况下被调用:

  1. libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。
  2. 程序执行exit函数时。
  3. 程序从main函数返回时。

源码:

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
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //如果输出缓冲区有数据,刷新输出缓冲区
result = EOF;


fp = fp->_chain; //遍历链表
}
[...]
}

image-20250420203533418

当满足以下条件,就会执行_IO_OVERFLOW()

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

所以在2.23的时候就可以这样构造来打FSOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
._chain => chunk_addr
chunk_addr
{
file = {
_flags = "/bin/sh\x00", //对应此结构体首地址(fp)
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x1,
...
_mode = 0x0, //一般不用特意设置
_unused2 = '\000' <repeats 19 times>
},
vtable = heap_addr
}
heap_addr
{
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x0,
__overflow = system_addr,
...
}

这时候chunk_addr里面的 _IO_write_ptr > _IO_write_basebase=0,就会触发chunk_addr的 _IO_OVERFLOW函数,执行 _IO_OVERFLOW(fp),也就是system(‘/bin/sh\x00’)

libc-2.24加入了对虚表的检查IO_validate_vtable()IO_vtable_check(),若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle。

1
2
3
4
5
6
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
1
2
3
4
5
6
7
8
9
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
_IO_vtable_check ();
return vtable;
}

glibc中有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址。
若指针不在glibcvtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。
具体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void attribute_hidden _IO_vtable_check (void)
{
#ifdef SHARED
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check) //检查是否是外部重构的vtable
return;

{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE)) //检查是否是动态链接库中的vtable
return;
}

...

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

因此,最好的办法是:我们伪造的vtableglibcvtable段中,从而得以绕过该检查。
目前来说,有四种思路:利用_IO_str_jumps_IO_str_overflow()函数,利用_IO_str_jumps_IO_str_finish()函数与利用_IO_wstr_jumps中对应的这两种函数,先来介绍最为方便的:

利用_IO_str_jumps_IO_str_overflow()函数的手段。(2.28)

_IO_str_jumps的结构体如下:

1
2
3
4
5
6
7
8
9
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
...
}

其中,_IO_str_finish源代码如下:

1
2
3
4
5
6
7
void _IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}

其中相关的_IO_str_fields结构体与_IO_strfile_结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

可以看到,它使用了IO结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。(这里_IO_overflow在下一次malloc中触发,把_IO_overflow修改为system,_s._free_buffer修改为/bin/sh的地址;或者把_IO_overflow修改成one_gadget)

利用_IO_str_jumps_IO_str_finish()函数的手段。(2.28)

我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps0x10,而_IO_OVERFLOW0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0
最后构造fp->_s._free_buffersystem_addrone gadget即可getshell
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumpsvtable中的倒数第二个表,用vtable的最后地址减去0x168定位

总结:修改 ((_IO_strfile *) fp)->_s._free_buffer 为 system 地址,然后修改 fp->_IO_buf_base 为 /bin/sh 字符串地址,然后触发程序执行 _IO_str_finish 函数就可以得到 shell 。

house of orange(2.23)

image-20250506192817097

当进行unsortedbins_attack时,这里的 _IO_list_all 的值会被修改为top,因为IO_list_all包含IO_FILE和IO_str_jumps两个结构体,然后此时smallbin[4]的fd字段,这时候这个字段就会对应 ._chain的值——就是说,此时smallbins[4]的fd字段对应着_IO_FILE这个结构体,同时里面是被我们放入smallbin的topchunk,这时候只要布置topchunk就是在伪造IO_FILE(然后接)

stdin劫持实现任意地址写

scanffreadgets等读入走IO指针(read不走)。

_IO_2_1_stdin结构体

1
extern struct _IO_FILE_plus _IO_2_1_stdin_

一般设置flag位为0xfbad2887

(1) 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS_flag &~ 0x4(一般不用特意设置)。
(3) 设置_fileno0(一般不用特意设置)。
(4) 设置_IO_buf_basewrite_start_IO_buf_endwrite_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。

stdout 任意地址读写

1
extern struct _IO_FILE_plus _IO_2_1_stdout_

printffwriteputs等输出走IO指针(write不走)。

(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
(3) 设置_fileno1
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。
一般来说,经常利用puts函数加上述stdout任意读的方式泄露libc

比如:泄露 libc 基址
对于没有输出功能的堆题,要想泄露 libc 基址就需要劫持 _IO_2_1_stdout_ 结构体,在libc-2.23 版本可以利用 fast bin attack 在 _IO_2_1_stdout_-0x43处申请 fast bin。然后把write_base最后一位改小,下一次输出就会先输出write_buf

全缓冲
(1)设置flag位(0xfbad1800)
(2)设置_IO_write_end指向write_end_IO_write_ptr指向write_start
(3)设置其他为0
写的内容是输出函数的参数(比如printf(%s,&a)就会把a的值写入目标地址)

三、流劫持


IO_FILE相关笔记
http://example.com/2025/04/20/IO_FILE相关笔记/
发布于
2025年4月20日
许可协议