IO_FILE 浅析
前言
glibc 2.34 之后
__malloc_hook/__free_hook被移除,利用面随之转向 IO_FILE。 这篇把_IO_FILE的结构和几个关键字段整理一遍,方便回头查。
_IO_FILE
struct _IO_FILE
{
int _flags; /* 高位字为_IO_MAGIC,剩余的部分是flag */
char *_IO_read_ptr; /* 正在使用的input缓冲区的input地址 */
char *_IO_read_end; /* input缓冲区的结束地址 */
char *_IO_read_base; /* input缓冲区的基址 */
char *_IO_write_base; /* output缓冲区的基址 */
char *_IO_write_ptr; /* 指向还没输出的那个字节 */
char *_IO_write_end; /* output缓冲区的结束地址 */
char *_IO_buf_base; /* input和output缓冲区的基址 */
char *_IO_buf_end; /* input和output缓冲区的结束地址 */
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; /* 存放着一个单链表,用于串联所有的file stream */
int _fileno; /* 与文件相关的文件描述符 */
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset; /* 存放虚表(virtual table)的偏移 */
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset; /* 存放当前文件的偏移 */
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
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)];
};
结构体字段很多,但真正在利用里反复出现的是这几个:
_flags:高 32 位是_IO_MAGIC(0xFBAD0000),低位是一组状态位(_IO_CURRENTLY_PUTTING、_IO_NO_WRITES之类),决定接下来的读写走哪条分支。FSOP 的第一步往往就是把它改成一个「看起来正常」的值。_IO_read_ptr/_IO_read_end/_IO_read_base:输入缓冲区的当前指针、末尾和基址。_IO_write_base/_IO_write_ptr/_IO_write_end:输出缓冲区三件套。fwrite、puts落到底层时写的是[_IO_write_base, _IO_write_ptr)这段,把base调小、ptr调大就能顺带泄露内存。_IO_buf_base/_IO_buf_end:缓冲区边界,分配/扩容缓冲时用。_chain:把所有FILE结构串成一条链表,_IO_list_all指向表头。FSOP 就是把伪造的结构挂到这条链上,等退出流程遍历链表时触发。_lock、_mode、_wide_data:加锁、宽字符流相关,走_IO_wfile_jumps那条路时会用到。
_IO_FILE_plus
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
这里的 FILE 就是 _IO_FILE,只是用 typedef 换了个名字:
#ifndef __FILE_defined
#define __FILE_defined 1
struct _IO_FILE;
/* The opaque type of streams. This is the definition used elsewhere. */
typedef struct _IO_FILE FILE;
#endif
所以一个 _IO_FILE_plus 就是 _IO_FILE 本体加一个 vtable 指针。在堆上伪造这个结构时,通常只需要让 _flags 和 vtable 看起来合法,其余字段按具体利用路径填。
_IO_jump_t
_IO_FILE_plus 的后半部分是一个指向 _IO_jump_t 的指针。
所谓 vtable 是 virtual table(虚函数表)的缩写,这里存放的是一组函数指针,调用 fread、fwrite 这类函数时会先查这张表。
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);
};
glibc 2.24 起对 vtable 加了校验:指针必须落在 __libc_IO_vtables 段内,直接改成任意地址会被 _IO_vtable_check 拦下。于是后续的利用转向 _wide_data->_wide_vtable——_IO_wfile_overflow 及其调用链会从那里取函数指针,早期版本没有同样的检查。