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?