IO_FILE利用

FILE结构

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 FILE 结构的指针来接收这个返回值。
首先看一下IO_FILE的源码,位于glibc/libio/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
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
};

进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。
stdin、stdout、stderr三个文件流的符号如下:

1
2
3
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

而在IO_FILE外,其实还嵌套着另一层结构,结构体定义如下

1
2
3
4
5
6
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
////32位下vtable在结构体中的偏移是0x94,而64位下偏移是0xd8

可以看到IO_FILE的同级还存在着一个名为vtable的指针,其指向了一个函数表,其中存放着一些函数指针

接下来我们用gdb调试一个程序实际去看一下,运行图如下:

[!figure]

图一

[!figure]

图二

这些就是vtable存放的指针。

利用stdout泄露libc

要利用stdout泄露libc,总的来说分为两步,一是修改stdout的_flags,二是修改_IO_write_base为目标打印地址

_flags

先来解析一下_flags的规则
由图一我们可以看到_flags是IO_FILE的第一个成员变量,并且其在整个利用过程中起着至关重要的作用,而_flags高两位字节是固定的,基本都是0xfbad0000,而我们要做的就是篡改其低二位字节,使flags变为0xfbad1800的固定值,至于为何是这个值,详解如下:
先来大概了解一下低二位的具体规则,一会儿会用到(不需要记忆,仅做了解即可,源码在glibc/libio/libio.h)

[!figure]
图三

再来了解一下puts函数的详细调用链: _IO_puts –> _IO_new_file_xsputn –> _IO_file_overflow
_IO_file_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
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;//为了避免程序进入这个分支导致死掉,所以f->_flags & _IO_NO_WRITES(该值为0x0008,见图三)要等于0,所以flag=0xfbad0000
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
......
//这个分支最后程序也会死掉,我们不能进去,所以f->_flags & _IO_CURRENTLY_PUTTING=1即可,所以flag=0xfbad0800
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base); //目标函数,在这里会调用write(1,buf,size),这里的第二个参数即为要打印的地址,所以我们可以将_IO_write_base修改为目标地址
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}

到这里还没有结束,接下来我们看一下进入_IO_do_write后的源码:(位置在glibc/libio/fileops.c)

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
new_do_write (FILE *fp, const char *data, size_t to_do)
{
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)
{
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;
}
//到这里是一个二进一结构,我们需要避免进入第二个(大概率会使程序死掉),因此只需要满足第一个的条件即可,即fp->_flags & _IO_IS_APPENDING==1,因此_flags=0xfbad1800

count = _IO_SYSWRITE (fp, data, to_do);//这里是最终目标
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

综上分析,我们需要将flags修改为固定的0xfbad1800(直接记住即可)

_IO_write_base与_IO_write_ptr

如上文分析,我们最终想要执行的是_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base)
因此只需要将_IO_write_base改为想要泄露的地址,将_IO_write_ptr改为泄露结束的地址即可

利用场景

以上的分析看似很复杂,实则在实际利用中只需要修改_flags与_IO_write_base,_IO_write_ptr即可,完全不需要记忆那些复杂的判断规则,毕竟_flags的修改目标是一个固定的值
而关于这种方法的利用,大多是因为没有可用的输出函数来泄露地址,就比如 HITCON 2018 PWN baby_tcache 这道程序,菜单里只有add与delete功能,而没有show功能来供我们利用,因此便可以用这种方法泄露libc,关于这道题的详细教程,可能会在之后文章中补充,这里便不过多赘述了