骑麦兜看落日

[Binary]IO_FILE

字数统计: 2.8k阅读时长: 13 min
2018/09/13 Share

IO_FILE


IO_FILE结构

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。

所有的文件流通过链表连接,全局变量_IO_list_all指向链表头部

IO_FILE结构包含于_IO_FILE_plus结构体中,其定义为

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

其中_IO_FILE定义了文件结构

偏移 属性 作用
0x00 _flags 高四位为魔数0xfbad0000,低四位为标志符
0x08 _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 标志符
0x80 _cur_column
0x88 _IO_stdfile_1_lock 锁结构体
0x90 _offset 文件描述符的偏移
0x98
0xa0 _IO_wide_data_1 宽字节流
0xa8 _freeres_list
0xb0
0xb8
0xc0 _mode 标记是否为宽字节
0xc8
0xd0

_IO_jump_t是一个虚函数表

偏移 hook 函数 作用
0x00 dummy
0x08 dummy2
0x10 finish 清理_IO_FILE对象
0x18 overflow 刷新缓冲区
0x20 underflow 返回get缓冲区的下一个字节
0x28 uflow 返回输入流的下一个字节
0x30 pbackfail 处理备份操作
0x38 xsputn 向缓冲区写N个字符
0x40 xsgetn 从缓冲区读N个字符
0x48 seekoff 将流位置移动到新位置
0x50 seekpos 将流位置移动到新的绝对位置
0x58 setbuf 为文件开辟缓冲区
0x60 sync 将文件内部数据结构与外部状态同步
0x68 doallocate 告诉文件分配缓冲区
0x70 sysread 读数据
0x78 syswrite 写数据
0x80 sysseek
0x88 sysclose 结束文件
0x90 sysstat
0x98 showmany
0xa0 imbue

在程序启动时会创建三个文件流stdinstdoutstderr,并且这三个文件流位于libc的数据段,而使用fopen创建的文件流位于堆中


FSP

一些与I/O操作相关的函数会调用虚函数表中的函数,FSP通过控制FILE结构,劫持vtable指向我们伪造的虚函数表,通过调用相关I/O函数时完成攻击

对于输入函数,会调用虚函数表中的uflowunderflowdoallocate,将输入流_IO_2_1_stdin_vtable指向我们伪造的虚函数表中,并将我们控制的内存覆盖伪造的虚函数表中

对于输出函数,会调用虚函数表中的xsputnoverflow,将输出流_IO_2_1stdoutvtable指向我们伪造的虚函数表中,并将我们控制的内存覆盖伪造的虚函数表中


FSOP

FSOP通过劫持_IO_list_all伪造一个IO_FILE结构体及虚函数表,然后触发_IO_flush_all_lockp函数刷新文件流,调用虚函数表中的overflow函数

_IO_flush_all_lockp函数由系统进行调用,调用发生在

  • Glib abort routine
  • exit function
  • Main return

为了顺利调用_IO_OVERFLOW,需要满足一定条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// glibc/libio/genops.c

int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;

for (fp = (_IO_FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{

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)
result = EOF;
}
return result;
}

我们需要满足

1
(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

或者

1
(_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

IO_FILE缓冲区攻击

puts函数

任意读

puts函数会先将数据复制到缓冲区,然后输出缓冲区数据,因此如果可以控制缓冲区便可以泄漏任意地址

现在看一下能够输出的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
// glibc/libio/ioputs.c

int
_IO_puts (const char *str)
{
int result = EOF;
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
return result;
}

_IO_puts函数首先调用_IO_new_file_xsputn函数

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
// glibc/libio/fileops.c
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */

/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
}
return n - to_do;
}

这里需要保证count<=0,否则会将输出数据复制到缓冲区,所以需要绕过if...else if…,即

1
2
!(f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)
!(f->_IO_write_end > f->_IO_write_ptr)

绕过后就成功进入了_IO_new_file_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
return (unsigned char) ch;
}

前边有两个if需要绕过,第一个会直接返回,第二个会修改掉构造好的f->_IO_write_basef->_IO_write_ptr

1
2
!(f->_flags & _IO_NO_WRITES)
(f->_flags & _IO_CURRENTLY_PUTTING)

绕过后会调用_IO_do_write,其中f是文件结构体,f->_IO_write_base是输出的起始地址,f->_IO_write_ptr - f->_IO_write_base是输出长度

1
2
3
4
5
6
int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

判断to_do不等于0后跳转到new_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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
return count;
}

这里需要绕过else if,不过由于绕过条件比较复杂一点,可以直接执行if子句绕过

1
(fp->_flags & _IO_IS_APPENDING)

最终调用_IO_SYSWRITE函数泄露出我们伪造的起始地址f->_IO_write_base和结束地址f->_IO_write_ptr,并且f->fileno=1

总结一下几个点

1
2
3
4
5
6
7
f->_IO_write_base	<- fake_addr
f->_IO_write_ptr <- fake_addr_end
!(f->_IO_write_end > f->_IO_write_ptr)
!((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) /* 0x200 */ /* 0x800 */
!(f->_flags & _IO_NO_WRITES) // 0x8
(f->_flags & _IO_CURRENTLY_PUTTING) // 0x800
(fp->_flags & _IO_IS_APPENDING) // 0x1000

任意写

在分析puts函数时会发现有一段代码调用了__mempcpy, 按照这个思路我们可以劫持缓冲区地址来实现任意写

首先是puts函数调用_IO_puts->_IO_new_file_xsputn,这个函数中将要输出的内容复制到了缓冲区

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
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */

/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
return n - to_do;
}

这里也没有什么特殊的限制,只需要将f->_IO_write_ptr指向需要覆盖的函数,通过f->_IO_write_end控制大小

gets函数

绕过vtable_check的攻击

在glibc2-24中增加了对vtable的检查,其源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// glibc-libio/libioP.h

/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
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))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

IO_validate_vtable函数会检查vtable指针是否位于__stop___libc_IO_vtables - __start___libc_IO_vtables,vtable位于__Libc_IO_vtables+0xb40

如果没有则调用_IO_vtable_check函数对vtable进行进一步检查,并在必要时终止程序,这个检查绕过是不可能绕过的,调用所有的虚函数都会进行一次检查

既然无法绕过,不如就利用__libc_IO_vtables中现成的函数来利用

_IO_str_jumps

_IO_str_jumps位于__libc_IO_vtables+0xc00,其定义如下

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
// glibc/libio/strops.c

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),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

这个虚函数表包含于结构体_IO_strfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// glibc/libio/strfile.h

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

struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};

struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

_IO_str_overflow

_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
29
30
31
32
33
34
// glibc/libio/strops.c

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
}
}
}
return c;
}

这个函数中调用了

1
(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

不过为了能够顺利执行到_allocate_buffer函数还需要满足条件

1
2
3
4
5
!(fp->_flags & _IO_NO_WRITES)		// 0x8
!(fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING) /* 0x400 0x800 */
(pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
=> fp->_IO_write_ptr - fp->_IO_write_base > ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + c
!(fp->_flags & _IO_USER_BUF) // 0x1

同时还要注意满足触发的条件

_IO_str_finish

_IO_str_jumps结构体中包含了_IO_str_finish函数

1
2
3
4
5
6
7
8
9
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);
}

这个函数中调用了

1
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)

不过为了能够顺利执行到_free_buffer函数还需要满足条件

1
(fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))

_IO_wstr_jumps

_IO_wstr_jumps位于__libc_IO_vtables+0x3c0`,其定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_wstr_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
JUMP_INIT(seekoff, _IO_wstr_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_wdefault_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

这个虚函数表同样包含于结构体_IO_strfile,利用方法和_IO_str_jumps相同,不再赘述

_IO_wstr_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
_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
int flush_only = c == WEOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : WEOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
}
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only))
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
_IO_size_t new_size = 2 * old_wblen + 100;

if (__glibc_unlikely (new_size < old_wblen)
|| __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
return EOF;

new_buf
= (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
* sizeof (wchar_t));
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return WEOF;
}
if (old_buf)
{
__wmemcpy (new_buf, old_buf, old_wblen);
}

}
return c;
}

_IO_wstr_finish

1
2
3
4
5
6
7
8
9
void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base);
fp->_wide_data->_IO_buf_base = NULL;

_IO_wdefault_finish (fp, 0);
}

参考资料

CATALOG
  1. 1. IO_FILE
    1. 1.1. IO_FILE结构
    2. 1.2. FSP
    3. 1.3. FSOP
    4. 1.4. IO_FILE缓冲区攻击
      1. 1.4.1. puts函数
        1. 1.4.1.1. 任意读
        2. 1.4.1.2. 任意写
      2. 1.4.2. gets函数
    5. 1.5. 绕过vtable_check的攻击
      1. 1.5.1. _IO_str_jumps
        1. 1.5.1.1. _IO_str_overflow
        2. 1.5.1.2. _IO_str_finish
      2. 1.5.2. _IO_wstr_jumps
        1. 1.5.2.1. _IO_wstr_overflow
        2. 1.5.2.2. _IO_wstr_finish
    6. 1.6. 参考资料