# 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)
待施工。。。