# IO_FILE
# 结构体及部分函数源码分析
# FILE结构体
FILE结构定义在libio.h中
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
};
_IO_FILE结构外包裹着另一种结构_IO_FILE_plus,在这个结构体中还包含着一个_IO_jump_t类型的指针vtable。
//_IO_jump_t结构体源码:
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
};
......
//_IO_FILE_plus及vtable指针定义:
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
......
在_IO_jump_t结构体中包含了一系列指针,在调用fopen、fclose、fread等一系列函数时就会调用这些函数指针
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
可以通过gdb打印来查看vtable中的指针:
pwndbg> print _IO_file_jumps
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e540d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7e54f00 <_IO_new_file_overflow>,
__underflow = 0x7ffff7e54ba0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7e560d0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7e57800 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7e53750 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7e533c0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e529e0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7e56780 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e526b0 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e52540 <_IO_new_file_sync>,
__doallocate = 0x7ffff7e45df0 <__GI__IO_file_doallocate>,
__read = 0x7ffff7e53720 <__GI__IO_file_read>,
__write = 0x7ffff7e52fe0 <_IO_new_file_write>,
__seek = 0x7ffff7e52780 <__GI__IO_file_seek>,
__close = 0x7ffff7e526a0 <__GI__IO_file_close>,
__stat = 0x7ffff7e52fc0 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e57990 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e579a0 <_IO_default_imbue>
}
当程序执行fopen等函数时就会创建FILE结构体,当我们打开多个文件,就会创建多个FILE结构体,这时候这些FILE结构体就会通过_chain域相连,链表头部用全局变量_IO_list_all表示,
当程序启动时会打开三个文件流:stdin、stdout、stderr。在初始状态下,_IO_list_all指向的是一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于libc.so的数据段。而我们使用fopen创建的文件流是分配在堆内存上的。
# fopen
fopen用于打开文件,原型如下:
FILE *fopen(char *filename, *type);
//返回一个文件指针
fopen函数实际调用的是_IO_new_fopen函数,_IO_new_fopen函数又会调用_fopen_internal函数,__fopen_internal函数主要分为四步:
1.malloc分配内存空间。
2.调用_IO_no_init函数对file结构体进行null初始化。
3.调用_IO_file_init函数将结构体链接进_IO_list_all链表。
4.调用_IO_file_fopen函数最终执行系统调用打开文件。
# fread
fread从文件流中读取数据,原型如下:
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
fread函数实际调用的是_IO_fread函数,_IO_fread函数又调用_IO_sgetn函数,这个函数又继续调用_IO_XSGETN函数,其实就是vtable中的__xsgetn函数。
__xsgetn函数主要分为三步:
1.当fp->_IO_buf_base==NULL时,说明FILE结构体中的指针还没有被初始化,缓冲区未建立,则调用_IO_doallocbuf初始化指针,建立缓冲区。
2.当输入缓冲区里有数据,即fp->_IO_read_ptr小于fp->_IO_read_end时,把缓冲区里的数据直接拷贝至目标buf。
3.当缓冲区里的数据为空或者是不能满足全部的需求,则调用__underflow调用系统调用读入数据。
fread在执行系统调用前执行了多次vtable中的函数。
__GI__IO_file_xsgetn
_IO_file_doallocate //初始化输入缓冲区
__GI__IO_file_stat //获取文件信息
_IO_new_file_underflow //读取文件数据
__GI__IO_file_read //最终执行系统调用
# fwrite
fwrite向文件流中写入数据,原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
fwrite函数实际调用的是_IO_fwrite函数,_IO_fwrite函数又调用了_IO_sputn函数,即vtable中的__xsputn函数。
这个函数也分为三部分:
1.判断输出缓冲区还有多少空间,如果输出缓冲区有剩余空间的话,把目标输出数据拷贝到输出缓冲区,然后计算在输出缓冲区填满后,是否仍然剩余的目标输出数据。
2.如果还有剩余的目标输出数据,说明输出缓冲区满了或者没有建立,这时候调用_IO_OVERFLOW刷新或建立输出缓冲区。
3.经过刷新或建立输出缓冲区后,检查目标输出数据的大小,如果超出输出缓冲区的大小,就跳过输出缓冲区直接输出。然后把最后剩余的数据拷贝到输出缓冲区。
fwrite在执行系统调用前同样执行了多次vtable中的函数
_IO_new_file_xsputn
_IO_new_file_overflow //刷新或建立输出缓冲区
_IO_file_doallocate //初始化输入缓冲区
__GI__IO_file_stat //获取文件信息
_IO_new_file_write //最终执行系统调用
# fclose
fclose用于关闭已打开的文件,原型如下:
int fclose(FILE *stream)
执行fclose后会关闭一个文件流,把缓冲区内剩余的数据输出到文件中,同时释放文件指针和有关的缓冲区。
fclose函数实际调用的是_IO_new_fclose函数,_IO_new_fclose函数大致分为四部分:
1.调用_IO_un_link函数将IO FILE结构体从_IO_list_all链表中取下。
2.调用_IO_file_close_it函数关闭文件,释放缓冲区,并清空缓冲区指针。
3.调用_IO_FINISH函数,即vtable中的__finish函数,确认文件开关状态和缓冲区是否被释放。
4.此时已经将结构体从链表中删除,刷新了缓冲区,释放了缓冲区内存,只剩下结构体内存尚未释放,因此释放结构体内存。
fclose函数执行过程中调用的vtable函数
__close //执行系统调用关闭文件描述符
__finish //确认文件开关状态和缓冲区状态
# 部分函数在libc中的函数名:
p *(struct _IO_FILE_plus *) stdout
p *_IO_list_all
p _IO_file_jumps
_IO_new_file_init
_IO_new_file_fopen
_IO_file_open
_IO_file_xsgetn
_IO_new_file_xsputn
_IO_new_file_close_it
# 劫持vtable并修改指针
vtable是_IO_FILE_plus结构体里的一个字段,是一个函数表指针,里面存储着许多和IO相关的函数。
fread、fwrite、fclose等IO函数基本都调用了vtable中的函数来实现功能。
# 2.23
# vtable劫持原理:
控制FILE结构体,实现对vtable指针的修改,使得vtable指向可控的内存,在该内存中构造好vtable,再通过调用相应IO函数,触发vtable函数的调用,即可劫持程序执行流。
# 思路:
1.修改内存中已有FILE结构体的vtable字段。
2.伪造整个FILE结构体。
本质都是修改了vtable字段。
# 例子
#define system_ptr 0x7ffff7a52390;
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;
fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40);//开辟一个堆当作伪造的vtable。
vtable_addr=(long long *)((long long)fp+0xd8); //0xd8为64位下vtable在_IO_FILE_plus结构体中的偏移。
vtable_addr[0]=(long long)fake_vtable;//让vtable指针指向我们伪造的vtable。
memcpy(fp,"sh",3);//因为vtable中的函数调用时会把对应的_IO_FILE_plus指针作为第一个参数传递,也就是_flags成员变量,所以这里设置_flag="sh"。
fake_vtable[7]=system_ptr; //找到__xsputn函数在vtable中的偏移,修改这个位置的指针,使其指向system函数。
fwrite("hi",2,1,fp);//执行fwrite函数会调用__xsputn函数,实际上是执行system("sh")
fclose(fp);
}
# 2.24以后
# vtable check机制
在执行_IO_OVERFLOW函数前执行了IO_validate_vtable函数,这个函数会检测vtable是不是glibc中的vtable,如果不是,进入_IO_vtable_check函数。
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
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;
}
#else /* !SHARED */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
//如果都不是,就报错,并且结束程序
}
总结:
1.判断vtable的地址是否位于glibc中的vtable数据段,若是,通过检查,返回。
2.判断vtable是否为外部合法的vtable(通过FILE*结构体创建的vtable等),若是,通过检查,返回。
3.若前两项检查都不通过,报错,退出程序。
有了这个机制后,我们伪造的vtable本身就位于堆栈上,肯定会进入_IO_vtable_check函数检查,并且修改vtable指针的行为通过不了这个检查,最终报错。
# 绕过vtable check机制
# vtable覆盖为外部地址
因为check机制的存在,我们如果还想伪造vtable,就需要满足下面两个条件的其中一个,分别对应_IO_vtable_check函数的两次检查条件。
1、flag == &_IO_vtable_check
2、_dl_open_hook!= NULL
第一个条件,flag的生成与canary类似,并没有固定的值,想要让flag == &_IO_vtable_check是比较困难的。
第二个条件,如果能往_dl_open_hook中写值,那完全可以往其他hook中写值,有更简单的方法。
# 利用另一个vtable函数(_IO_str_jumps)
p _IO_str_jumps
这个vtable中有两个函数,_IO_str_overflow和_IO_str_finish。其中_IO_str_finish函数在满足if语句的情况下直接使用了fp->_s._free_buffer指针,如果修改这个指针为one_gadget,就能getshell。
# 如何利用
-
绕过_IO_flush_all_lokcp中对输出缓冲区的检查,让程序进入_IO_OVERFLOW函数。
-
要绕过vtable check机制,可以把vtable的地址覆盖成_IO_str_jumps-8的地址(_IO_str_jumps的偏移为0x10,_IO_OVERFLOW的偏移为0x18),_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数,同时因为_IO_str_finish函数位于vtable的地址段中,所以可以绕过vtable check。_
-
_满足_IO_str_finish函数的if语句,让程序使用fp->_s._free_buffer指针。因此要满足fp->_IO_buf_base不为NULL并且fp->_flags中不包含_IO_USER_BUF。(#define _IO_USER_BUF 1;)
-
构造fp->_s._free_buffer为system/gadget,fp->_IO_buf_base为"/bin/sh",调用(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)时就相当于执行了system("/bin/sh")。
# 劫持vtable及FSOP
FSOP全称是File Stream Oriented Programming,利用的是前面fopen函数中描述过的_IO_list_all指针。
我们知道程序中所有打开的文件流都是由一个单链表进行管理的,并且,链表是由结构体中的_chain字段进行连接。
在正常的程序中,会存在stderr、stdout、stdin三个IO_FILE,通过gdb打印
pwndbg> print _IO_list_all
$1 = (struct \_IO\_FILE\_plus *) 0x7ffff7dd2540 <\_IO\_2\_1\_stderr\_>
pwndbg> print _IO_list_all->file._chain
$2 = (struct \_IO\_FILE *) 0x7ffff7dd2620 <\_IO\_2\_1_stdout\_>
pwndbg> print (struct _IO_FILE *) (_IO_list_all->file._chain)._chain
$3 = (struct \_IO\_FILE *) 0x7ffff7dd18e0 <\_IO\_2\_1\_stdin\_>
pwndbg> print (struct _IO_FILE *) ((_IO_list_all->file._chain)._chain)._chain
$4 = (struct \_IO\_FILE *) 0x0
形成了下图所示的链表:

如果能够控制_IO_list_all指针,就可以让该指针直接指向我们想要的地址,这就是FSOP。
libc中存在一个函数_IO_flush_all_lockp,用来刷新所 有FILE结构体的输出缓冲区。
int _IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
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;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain; //遍历列表,检查所有结构体的输出缓冲区。
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
前面在分析fwrite的时候提到过,输出缓冲区的数据保存在fp->IO_write_base上,且长度为fp->_IO_write_ptr-fp->_IO_write_base,因此,这个函数的整体思路是检查该结构体的输出缓冲区是否还有数据,如果有就调用_IO_OVERFLOW去刷新输出缓冲区。如果可以控制_IO_list_all中的某一个节点,并构造vtable的话,就可以控制程序流。
攻击的整体流程就是:
- 伪造一个
_IO_FILE结构体,并使_IO_list_all链表指向该结构体,或者让_IO_list_all链表中某一节点的_chain字段指向伪造的数据。- 利用
_IO_flush_all_lockp函数,绕过检查,最终调用_IO_OVERFLOW来劫持程序流。
# 东华杯2016-pwn450-note(house of orange)
待施工。。。