_IO_FILE利用思路总结
_IO_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
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
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
};进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示,通过这个值我们可以遍历所有的FILE结构。
在标准I/O库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于libc.so的数据段。而我们使用fopen创建的文件流是分配在堆内存上的。
我们可以在libc.so中找到stdin\stdout\stderr等符号,这些符号是指向FILE结构的指针,真正结构的符号是
通常在使用printf/puts函数的时候,会使用_IO_2_1_stdout_结构;在使用scanf/gets的时候,会使用_IO_2_1_stdin_结构
事实上_IO_FILE结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针vtable指向了一系列函数指针。
64位IO_FILE_plus结构体中的偏移
vtable是_IO_jump_t类型的指针,_IO_jump_t中保存了一些函数指针,在后面我们会看到在一系列标准IO函数中会调用这些函数指针,该类型在libc文件中的导出符号是_IO_file_jumps。据我观察好像所有的_IO_FILE_plus的vtable指针都是同一指向(至少stderr,stdout,stdin是这样),这为漏洞利用提供了方便
下面简单说说一些c函数对_IO_jump_t虚表里面函数的调用情况
printf/puts 最终会调用
_IO_file_xsputnfclose 最终会调用
_IO_FILE_FINISHfwrite 最终会调用
_IO_file_xsputnfread 最终会调用
_IO_fiel_xsgetnscanf/gets 最终会调用
_IO_file_xsgetn
libc_2.24以下的利用
前面我们介绍了 Linux 中文件流的特性(FILE),我们可以得知 Linux 中的一些常见的 IO 操作函数都需要经过 FILE 结构进行处理。尤其是_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。
由于位于 libc 数据段的 vtable 是不可以进行写入的,所以我们只能伪造 vtable
伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。
所以,最常见的利用方法就是修改_IO_2_1_stdout_结构,因为printf时会用到该结构,且最终会调用到该结构vtable里面的_IO_file_xsputn函数指针。
so怎么定位_IO_2_1_stdout_结构?_IO_file_xsputn函数指针改成啥?
如果程序有调用setbuf或者setvbuf函数来设置stdin,stdout,stderr输入流等,那么程序的bss段上就会存在它的指针(但未初始化)如:

但通常情况下,可以泄露libc地址的情况下,我们能够在libc文件中找到直接找到_IO_2_1_stdout_
改函数指针我们可以填充为one_gadget,这样就不用考虑参数问题;若one_gadget的环境变量都不好使,可以考虑填充为system函数地址,传参的话,多数vtable函数指针在被调用时,会将它的_IO_FILE_plus地址当作第一个参数传递,所以我们可以将_IO_FILE_plus的_flags成员填成“/bin/sh\x00”,但这种方法通常也不好用,因为调用vtable函数指针之前会对_IO_FILE_plus的结构进行检查,通常改“/bin/sh\x00”之后会导致对_flags成员的检查不通过(亲测printf不行,但House of orange利用中出现的_IO_flush_all_lockp能检查通过)
FSOP利用
见我的另一篇文章https://b0ldfrev.top/2018/11/06/House-of-orange/#fsop
64位的_IO_FILE_plus构造模板:
32位的_IO_FILE_plus构造模板:
64位下seccomp禁用execve系统调用的构造模板:
将函数控制流控制在 setcontext+53 的位置,是因为这里正好可以修改 rsp 到我们的可控地址来进 行 rop,在切栈之后就可以按照如上过程执行 rop。 首先调用 mprotect 函数将 当前 heap 段设置为可执行,然后调用 cat flag 的 shellcode。
libc_2.24及以上的利用
glibc 2.24 对 vtable 做了检测,导致我们不能通过伪造 vtable 来执行代码,对 vtable 进行校验的函数是 IO_validate_vtable
vtable必须要满足 在 __stop___IO_vtables 和 __start___libc_IO_vtables 之间,而我们伪造的vtable通常不满足这个条件。
但是我们找到一条出路,那就是_IO_str_jumps 与__IO_wstr_jumps就位于 __stop___libc_IO_vtables 和 __start___libc_IO_vtables 之间, 所以我们是可以利用他们来通过 IO_validate_vtable 的检测的,只需要将vtable填成_IO_str_jumps 或__IO_wstr_jumps就行。
其中,利用 __IO_str_jumps 绕过更简单,__IO_str_jumps 结构如下, 和vtable结构类似,__IO_str_jumps里面都是相似名字的函数指针,但功能不一样,且代码存在可利用漏洞
下面列出几种可行方法,但不限于这几种:
利用
__IO_str_jumps中的_IO_str_finsh函数利用
__IO_str_jumps中的_IO_str_overflow函数
如何定位_IO_str_jumps?
由于 _IO_str_jumps 不是导出符号,因此无法直接利用 pwntool s的 libc.sym["_IO_str_jumps"] 进行定位,我们可以转换一下思路,利用 _IO_str_jumps中的导出函数,例如 _IO_str_underflow 进行辅助定位,我们可以利用gdb去查找所有包含这个_IO_str_underflow 函数地址的内存地址,如下所示:
再利用 _IO_str_jumps 的地址大于 _IO_file_jumps 地址的条件,就可以锁定最后一个地址为符合条件的 _IO_str_jumps 的地址,由于 _IO_str_underflow 在_IO_str_jumps 的偏移为0x20,我们可以计算出_IO_str_jumps = 0x7f4d4d2245c0,再减掉libc的基地址,就可以得到_IO_str_jumps 的正确偏移。 当然也可以用IDA Pro分析libc.so,查找_IO_file_jumps 后的jump表即可。 此外,介绍一种直接利用pwntools得到_IO_str_jumps 偏移的方法,思想与采用动态调试分析的方法类似,直接放代码(该方法在我自己的测试环境中GLIBC 2.23、2.24版本均测试通过):
利用_IO_str_finsh
所以在调用_IO_str_finsh时只需要满足条件
便可以call qword ptr [fp+E8h],于是我们把fp+E8处填成system_addr ,还有 fp->_IO_buf_base填上binsh_addr地址。至于怎么call到这个虚表函数,可以利用vtable也就是__IO_str_jumps地址错位的方法,详细见下面解说。
_IO_str_finsh在libc2.24及以上最常使用的场景之一是 伪造printf的_IO_FILE ,printf里面的vfprintf函数对_IO_FILE的check比较严格,但_IO_str_finsh对_IO_FILE的check和vfprintf几乎是相似的,不冲突。 printf最后调用到的是vtable+0x38处的_IO_xsputn,但_IO_str_finish的偏移为0x10,我们可以将虚表地址填成_IO_str_jumps - 0x28,这样就能调用到_IO_str_finish
利用_IO_str_overflow
利用_IO_str_overflow 比 _IO_str_finish复杂
来看看要绕过的地方
fp->_flags & 8 得为0,(fp->_flags & 0xC00) == 0x400 得为0 , fp->_flags & 1 得为0, 所以我这里就将fp->_flags设置为0 ; 并设置fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base
这样我们才能够绕过检查,来到函数调用(fp + 0xE0)(2 * v6 + 100)也就是 (fp + 0xE0)(2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100)
我们可以将 fp + 0x E0设置成 system函数地址 ,fp->_IO_buf_base设置成0,fp->_IO_buf_end设置成 (binsh_addr-100)/2这样就设置了system参数,fp->_IO_write_ptr设置成一个很大的正值
这里有个坑,就是(binsh_addr-100)/2, 当/bin/sh地址存在于libc中,它/2再*2 若是除不尽有余数的话 会影响最后的参数地址,我们为了避免这种情况我们尽量在堆内存中填入"/bin/sh\x00", 因为堆内存往往都是2的倍数对齐。
利用_IO_buf_end实现write
scanf()函数内部,当_IO_read_ptr >= _IO_read_end时,会最终调用read的系统调用向_IO_buf_base中读入数据。
有条件的话我们可修改_IO_2_1_stdin_的 _IO_buf_base 与 _IO_buf_end,这样在执行scanf读取数据到缓冲区时,就可以写入东西到_IO_buf_base
常见的利用方式就是,利用 unsorted bin attack 去改写file结构体中的某些成员,比如IO_2_1_stdin 中的 _IO_buf_end,这样在 _IO_buf_base 和_IO_buf_end(main_arena+0x58) 存在 __malloc_hook,可以利用scanf函数读取数据填充到该区域,注意尽量不要破坏已有数据。
scanf读取的payload如下:
PS:有个地方需要注意,如上方源码fp->_IO_read_end += count,当读取数据进入_IO_buf_base缓冲区后会导致_IO_read_end增大,这样第二次读入时很有可能_IO_read_ptr < _IO_read_end,这样就不会往_IO_buf_base写数据了,这时想第二次写数据可以多次使用getchar函数(调用一次_IO_read_ptr++),待_IO_read_ptr增大到等于fp->_IO_read_end时,再次调用scanf往_IO_buf_base写数据。
所以还有一种情况就是,任意地址写1null,我们先写_IO_buf_base的低1byte为\00,指针恰好指向了stdin内部地址,我们这时调用scanf即可以再次覆写_IO_buf_base为任意地址。之后要用getchar平衡_IO_read_ptr指针,待_IO_read_ptr == _IO_read_end后即可往任意地址写值。
利用IO_write_base实现leak
Tips: tcache下可利用IO_write_ptr
libc2.23讲解
puts函数由_IO_puts函数实现,其内部调用_IO_sputn,接着执行_IO_new_file_xsputn,最终会执行_IO_overflow
_IO_puts的相关源码:
_IO_new_file_overflow的相关源码:
这里如果f->_flags & _IO_CURRENTLY_PUTTING为零的话,里面的代码会进入if (f->_IO_read_ptr == f->_IO_buf_end)分支,将我们设置的_IO_write_base恢复,所以这里要让f->_flags & _IO_CURRENTLY_PUTTING为1.程序接下来下来调用输出函数 _IO_do_write,我们再接着看一下函数_IO_do_write,这个函数实际调用的时候会用到new_do_write函数,其参数与之前一样。
主要看函数的count赋值的那个地方,data=_IO_write_base,size=_IO_write_ptr - _IO_wirte_base就是这之间的距离,然后最后会return的count实现leak。ps:其中为了防止其进入else if 分支,会对文件指针进行校准,所以我们需要设置fp->_flags & _IO_IS_APPENDING返回1.
在setvbuf(stdout,0,2,0)后,输出流被设置成_IONBF(无缓冲):直接从流中读入数据或直接向流中写入数据,而没有缓冲区。我们就可以利用如下方式:
如图

申请到_IO_2_1_stderr_+160 该处 错位能构造出一个size域为0x7f的chunk;
将_IO_2_1_stdout_的flag填成p64(0xfbad1800)能过printf或者puts的check,其他项随意填充成相同的libc地址即可;
重要的是将IO_write_base处的低1字节填充成“\x50”(缩小base的值);
使其在下次puts时输出我们修改后的_IO_write_base到_IO_write_ptr的数据。这样就能泄露libc地址。
libc2.27-tcache讲解
对于存在tcache机制的利用方法其实就很简单了,因为不用像2.23那样必须分配到伪造合适size的chunk,再覆盖stdout,我们可以直接分配到_IO_write_ptr,直接覆盖_IO_write_ptr低1byte为"\xff"。
这样的话我们不用考虑没有伪造flags的_IO_CURRENTLY_PUTTING域,在_IO_new_file_overflow函数中进入if (f->_IO_read_ptr == f->_IO_buf_end)分支,重置_IO_write_指针
也不用考虑进入new_do_write函数时,进入else if (fp->_IO_read_end != fp->_IO_write_base)分支对文件指针进行校验,因为此时我们没有修改_IO_read_end 与_IO_write_base的值。
由于之前我们把_IO_write_ptr低1字节覆盖成"\xff",所以在调用_IO_SYSWRITE (fp, data, to_do)时,data是_IO_wirte_base(base不变),to_do是IO_write_ptr - _IO_wirte_base(ptr变大了),同样能打印中间的值,所以同样可泄露。
Last updated
Was this helpful?