-
-
[原创]_IO_FILE劫持的一点总结
-
2021-1-3 20:47 7034
-
本文首先介绍文件流中涉及到的重要struct,并从fopen打开一个文件开始分析了打开文件的过程,之后通过分析fread函数调用,并从汇编层面分析vtable函数跳转表执行的过程,给出可利用方案,最后分析了libc对该劫持的漏洞利用缓解措施。本文源码参考glibc-2.23版本
文件流中的重要结构体
- IO_FILE 的结构体如下
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; _IO_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)]; #endif };
- IO_FILE_PLUS 的 结构体
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };;
- 通过 fopen 打开文件的时候,会在 堆上 为该文件申请一片 locked_FILE 结构体的空间,用来存储该文件的文件流信息
struct locked_FILE { struct _IO_FILE_plus fp; _IO_lock_t lock; struct _IO_wide_data wd; } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
- 随后会初始化 结构体中fp的值
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps); _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
- 将该堆结构链入全局变量 _IO_list_all
通过_IO_file_init将fp链入全局输入输出流链表_IO_list_all _IO_file_init (&new_f->fp); //该列表上的stdin,stdout,stderr是存储在libc中的,DEF_STDFILE是初始化其对应的结构体的 //_IO_file_init (&new_f->fp); DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES); DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED); struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_; //_IO_list_all可以通过libc的基地址计算出来
fread过程分析
fread的读取
- 读取的过程,fread会调用 iofread.c中的_IO_fread函数,继而会调用fileops.c中的_IO_file_xsgetn,在read的过程中,涉及到的_IO_FILE结构体中的指针有如下几个
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. */ // 缓冲区的大小,读取的内容存放的地方
- 首先要设置的是_IO_buf_base,以及 _IO_buf_end指针,如果_IO_buf_base的值为空,则通过_IO_doallocbuf 为_IO_buf_base申请一块内存,glibc2.23调试申请到的堆空间为4096byte,该空间将作为缓冲区从文件读取数据
glibc-2.23\glibc-2.23\libio\genops.c _IO_doallocbuf -> _IO_DOALLOCATE (fp) ===> _IO_file_doallocate F:\源码阅读\glibc-2.23\glibc-2.23\libio\filedoalloc.c int _IO_file_doallocate (_IO_FILE *fp){ ... p = malloc (size); if (__glibc_unlikely (p == NULL)) return EOF; _IO_setb (fp, p, p + size, 1); // __IO_setb会设置_IO_buf_base }
- 通过fp->_IO_read_end 和 _IO_read_ptr判断已经读取出的数据大小,若已经取出需要的数据
have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) // want为需要的字节长度,have为已读取到的字节长度,如果满足已经读取到的字节长度满足需要读取的字节长度,则将缓冲区中的数据cp到读取区域 { memcpy (s, fp->_IO_read_ptr, want);// 拷贝到缓冲区 fp->_IO_read_ptr += want; want = 0; }
- 如果已读取字节,分为未读取和 已经读取部分字节但是不满足需要的字节数两种情况
if (have > 0) { #ifdef _LIBC s = __mempcpy (s, fp->_IO_read_ptr, have); #else memcpy (s, fp->_IO_read_ptr, have); s += have; #endif want -= have; fp->_IO_read_ptr += have; }
- 通过 __underflow 从文件读取字节, 通过_IO_SYSREAD将文件内容读入缓冲区中,缓冲区的大小由_IO_buf_base和_IO_buf_end来决定,后续从缓冲区copy到堆空间上时,则通过_IO_read_base和_IO_read_end来决定。
\glibc-2.23\libio\genops.c __underflow -> _IO_UNDERFLOW ===> _IO_new_file_underflow \glibc-2.23\libio\fileops.c _IO_new_file_underflow _IO_new_file_underflow (_IO_FILE *fp){ ... fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;//__IO_read_base指向 _IO_buf_base fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; count = _IO_SYSREAD (fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base);//_IO_file_read fileops.c fp->_IO_read_end += count; ... }
- __underflow读取数据完后,循环进行下一次读操作
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; continue; }
劫持分析
vtable的调用
- vtable是一个指针,该指针指向的区域,存放了IO_jump_t 结构体 ,是一个跳转中转表
- 以fread为例,_IO_fread函数中调用了_IO_segetn 函数
_IO_size_t _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n) { /* FIXME handle putback buffer here! */ return _IO_XSGETN (fp, data, n); //fileops.c -> _IO_file_xsgetn }
- 对于 _IO_XSGETN 的调用,展开宏定义后
((*(__typeof__ (((struct _IO_FILE_plus){}).vtable) *)(((char *) ((fp))) + __builtin_offsetof (struct _IO_FILE_plus, vtable)))->__xsgetn) (fp, data, n)
- 其对应的汇编代码如下,其中,
sub rsp, 8 ; Alternative name is '_IO_sgetn' mov rax, [fp + 0D8h] ; fp的偏移0xd8的vtable的值,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8 call qword ptr [rax+40h] ; 调用指定函数,[rax+40h]指向libc上 add rsp, 8 retn ####
- 在[rax]处存储的内容如下,通过call 跳转到指定的地址,可以通过overwrite vtable的值,或者改写[rax + offset]处的内容来达到劫持程序流的目的
$47 = { __dummy = 0, __dummy2 = 0, __finish = 0x7ffff7ab81d3 <_IO_new_file_finish>, __overflow = 0x7ffff7ab8981 <_IO_new_file_overflow>, __underflow = 0x7ffff7ab8724 <_IO_new_file_underflow>, __uflow = 0x7ffff7ab96da <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7aba2ad <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7ab7c6b <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7ab7ebc <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7ab73a2 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7ab9907 <_IO_default_seekpos>, __setbuf = 0x7ffff7ab724b <_IO_new_file_setbuf>, __sync = 0x7ffff7ab7191 <_IO_new_file_sync>, __doallocate = 0x7ffff7aac841 <__GI__IO_file_doallocate>, __read = 0x7ffff7ab7b69 <__GI__IO_file_read>, __write = 0x7ffff7ab7bc6 <_IO_new_file_write>, __seek = 0x7ffff7ab7930 <__GI__IO_file_seek>, __close = 0x7ffff7ab7165 <__GI__IO_file_close>, __stat = 0x7ffff7ab7bad <__GI__IO_file_stat>, __showmanyc = 0x7ffff7aba3f5 <_IO_default_showmanyc>, __imbue = 0x7ffff7aba3fb <_IO_default_imbue> }
劫持程序流后的参数传递
- 以fread为例,当程序调用_IO_XSGETN 函数的时候,会通过查_IO_file_jumps表来解析出_IO_XSGETN对应的函数,当可以overwrite _IO_file_jumps表,或者劫持到vtable 指针的时候,就可以实现程序的控制流劫持
_IO_size_t _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n) { /* FIXME handle putback buffer here! */ return _IO_XSGETN (fp, data, n); }
- _IO_XSGETN 的跳转定义为JUMP2(FUNC,THIS,X1,X2) ,this传入的是FUNC,在代码中是fp的地址,所以劫持后的第一个参数的位置在fp的位置处
#在跳转的时候函数定义为JUMP2(FUNC,THIS,X1,X2) #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N) #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
- 劫持分两类讨论
- 第一种,对于stderr,stdout,stdin的劫持,只能overwrite _IO_FILE_plus结构体中的vtable指针,使其指向我们所伪造的跳转表,达到劫持目的,该方法也普适于其他的文件流。
- 第二种,通过overwrite跳转表中的函数指针来达到覆写的目的
- 要实现以上的任意读写,前提需要已知libc的基地址,并且程序漏洞可以实现任意地址写
libc_base_address -> _IO_list_all -> 任意结构体地址 -> 偏移计算vtable -> vtable表地址 libc_base_address -> _std_in/_std_out/_std_err地址 -> 偏移计算vtable -> vtable表地址
针对vtable的劫持缓解机制
- 在libc-2.24版本上,增加了对vtable的劫持检测机制,在Libc-2.23版本一下的跳转是如下实现的
//libc-2.23 # define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS) //libc-2.24 # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
- 在libc-2.24版本上,增加的检查机制IO_validate_vtable定义如下,如果offset大于 section_length,则进入_IO_vtable_check()来检查这个vtable是否合法。
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; }
新的利用方式
- 在libc-2.24版本以后(大于等于)针对IO_FILE文件的vtable劫持增加了检测机制使得无法再使用vtable来劫持程序流
- 但是IO_FILE的结构体中的_IO_buf_base,_IO_buf_end仍可以用来读取、写入数据
- 在libc中不仅仅只有_IO_file_jumps这么一个vtable,还有一个叫做_IO_str_jmps的,这个vtable不在check的范围内(引自ctf-wiki)
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2021-1-5 18:03
被seana编辑
,原因:
赞赏
他的文章
看原图