4.13 利用 _IO_FILE 结构
FILE 结构
FILE 结构体的利用是一种通用的控制流劫持技术。攻击者可以覆盖堆上的 FILE 指针使其指向一个伪造的结构,利用结构中一个叫做 vtable 的指针,来执行任意代码。
我们知道 FILE 结构被一系列流操作函数(fopen()、fread()、fclose()等)所使用,大多数的 FILE 结构体保存在堆上(stdin、stdout、stderr除外,位于libc数据段),其指针动态创建并由 fopen() 返回。在 glibc(2.23) 中,这个结构体是 _IO_FILE_plout,包含了一个 _IO_FILE 结构体和一个指向 _IO_jump_t 结构体的指针:
// libio/libioP.h
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
};
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
extern struct _IO_FILE_plus *_IO_list_all;vtable 指向的函数跳转表其实是一种兼容 C++ 虚函数的实现。当程序对某个流进行操作时,会调用该流对应的跳转表中的某个函数。
进程中的 FILE 结构会通过 _chain 域构成一个链表,链表头部用全局变量 _IO_list_all 表示。
另外 _IO_wide_data 结构也是后面需要的:
fopen
下面我们来看几个函数的实现。
fread
所以 _IO_XSGETN 宏最终会调用 vtable 中的函数,即:
fwrite
_IO_XSPUTN 最终将调用下面的函数:
fclose
FSOP
FSOP(File Stream Oriented Programming)是一种劫持 _IO_list_all(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 _IO_flush_all_lockp() 函数来触发,该函数会在下面三种情况下被调用:
libc 检测到内存错误时
执行 exit 函数时
main 函数返回时
当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW。
于是在 _IO_OVERFLOW(fp, EOF) 的执行过程中最终会调用 system('/bin/sh')。
还有一条 FSOP 的路径是在关闭 stream 的时候:
于是在 _IO_FINISH (fp) 的执行过程中最终会调用 system('/bin/sh')。
libc-2.24 防御机制
但是在 libc-2.24 中加入了对 vtable 指针的检查。这个 commit 新增了两个函数:IO_validate_vtable 和 _IO_vtable_check。
所有的 libio vtables 被放进了专用的只读的 __libc_IO_vtables 段,以使它们在内存中连续。在任何间接跳转之前,vtable 指针将根据段边界进行检查,如果指针不在这个段,则调用函数 _IO_vtable_check() 做进一步的检查,并且在必要时终止进程。
libc-2.24 利用技术
_IO_str_jumps
在防御机制下通过修改虚表的利用技术已经用不了了,但同时出现了新的利用技术。既然无法将 vtable 指针指向 __libc_IO_vtables 以外的地方,那么就在 __libc_IO_vtables 里面找些有用的东西。比如 _IO_str_jumps(该符号在strip后会丢失):
这个 vtable 中包含了一个叫做 _IO_str_overflow 的函数,该函数中存在相对地址的引用(可伪造):
所以可以像下面这样构造:
fp->_flags = 0
fp->_IO_buf_base = 0
fp->_IO_buf_end = (bin_sh_addr - 100) / 2
fp->_IO_write_ptr = 0xffffffff
fp->_IO_write_base = 0
fp->_mode = 0
有一点要注意的是,如果 bin_sh_addr 的地址以奇数结尾,为了避免除法向下取整的干扰,可以将该地址加 1。另外 system("/bin/sh") 是可以用 one_gadget 来代替的,这样似乎更加简单。
完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_overflow。
与传统的 house-of-orange 不同的是,这种利用方法不再需要知道 heap 的地址,因为 _IO_str_jumps vtable 是在 libc 上的,所以只要能泄露出 libc 的地址就可以了。
在这个 vtable 中,还有另一个函数 _IO_str_finish,它的检查条件比较简单:
只要在 fp->_IO_buf_base 放上 "/bin/sh" 的地址,然后设置 fp->_flags = 0 就可以了绕过函数里的条件。
那么怎样让程序进入 _IO_str_finish 执行呢,fclose(fp) 是一条路,但似乎有局限。还是回到异常处理上来,在 _IO_flush_all_lockp 函数中是通过 _IO_OVERFLOW 执行的 __GI__IO_str_overflow,而 _IO_OVERFLOW 是根据 __overflow 相对于 _IO_str_jumps vtable 的偏移找到具体函数的。所以如果我们伪造传递给 _IO_OVERFLOW(fp) 的 fp 是 vtable 的地址减去 0x8,那么根据偏移,程序将找到 _IO_str_finish 并执行。
所以可以像下面这样构造:
fp->_mode = 0
fp->_IO_write_ptr = 0xffffffff
fp->_IO_write_base = 0
fp->_wide_data->_IO_buf_base = bin_sh_addr (也就是 fp->_IO_write_end)
fp->_flags2 = 0
fp->_mode = 0
完整的调用过程:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_finish。
_IO_wstr_jumps
_IO_wstr_jumps 也是一个符合条件的 vtable,总体上和上面讲的 _IO_str_jumps 差不多:
利用函数 _IO_wstr_overflow:
利用函数 _IO_wstr_finish:
最新动态
来自 glibc 的 master 分支上的一次 commit,不出意外应该会出现在 libc-2.28 中。
该方法简单粗暴,用操作堆的 malloc 和 free 替换掉原来在 _IO_str_fields 里的 _allocate_buffer 和 _free_buffer。由于不再使用偏移,就不能再利用 __libc_IO_vtables 上的 vtable 绕过检查,于是上面的利用技术就都失效了。:(
CTF 实例
请查看章节 6.1.24、6.1.25 和 6.1.26。另外在章节 3.1.8 中也有相关内容。
附上偏移,构造时候方便一点:
参考资料
Last updated
Was this helpful?