IO FILE 之vtable check 以及绕过
字数 2076 2025-08-05 08:20:12

IO FILE vtable check机制及绕过方法详解

一、背景介绍

在libc 2.23及之前版本中,劫持vtable指针以及FSOP(File Stream Oriented Programming)是常见的利用方法。由于vtable包含众多功能强大的函数指针,缺乏保护机制显然不安全。因此,glibc在2.24版本引入了vtable check机制,使得传统的修改vtable指针指向可控内存的方法失效。

二、vtable check机制分析

1. 检查流程

当尝试修改vtable指针指向堆或栈地址时,glibc 2.24会报错:"Fatal error: glibc detected an invalid stdio handle"。检查机制的核心在_IO_vtable_check函数中:

  1. 第一层检查IO_validate_vtable函数验证vtable指针是否在glibc的vtable段中

    static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) {
        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))
            _IO_vtable_check();
        return vtable;
    }
    
  2. 第二层检查:如果vtable不在合法段中,则进入_IO_vtable_check函数

    void attribute_hidden _IO_vtable_check (void) {
        // 检查是否是外部重构的vtable或动态链接库中的vtable
        if (flag == &_IO_vtable_check || _dl_open_hook != NULL || ...)
            return;
        libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
    }
    

2. 检查机制总结

  1. 判断vtable地址是否在glibc的vtable数组段中
  2. 如果不在,则判断是否为外部合法vtable(重构或动态链接库中的vtable)
  3. 都不满足则报错退出

三、绕过vtable check的方法

1. 使用内部vtable:_IO_str_jumps_IO_wstr_jumps

_IO_str_jumps利用原理

_IO_str_jumps虚表中的_IO_str_finish函数存在可利用的代码:

void _IO_str_finish (_IO_FILE *fp, int dummy) {
    if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
        (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
    fp->_IO_buf_base = NULL;
    _IO_default_finish (fp, 0);
}

利用条件:

  1. 将vtable地址覆盖为_IO_str_jumps-8,使_IO_str_finish成为伪造的_IO_OVERFLOW函数
  2. 构造fp->_IO_buf_base不为NULL(通常设为"/bin/sh"地址)
  3. 设置fp->_flags最低位为0(不包含_IO_USER_BUF
  4. 设置_s._free_buffersystem或one gadget地址

利用步骤

  1. 通过FSOP触发_IO_flush_all_lockp中的_IO_OVERFLOW调用

    • 满足_mode <= 0_IO_write_ptr > _IO_write_base
  2. 伪造IO FILE结构体:

    • vtable指针指向_IO_str_jumps-8
    • _IO_buf_base指向"/bin/sh"字符串
    • _flags设为0
    • _s._free_buffer设为system地址
  3. 最终执行:system("/bin/sh")

_IO_str_jumps定位方法

  1. 通过vtable最后地址减去0x168
  2. 或通过_IO_wfile_jumps地址减去0x240(对于_IO_wstr_jumps

2. 使用缓冲区指针进行任意内存读写

(将在后续文章中详细讨论)

四、实践案例

案例1:hctf 2017 babyprintf

利用步骤:

  1. 使用格式化字符串泄露地址
  2. 通过堆溢出覆盖top chunk size
  3. 利用unsorted bin attack改写_IO_list_all
  4. 伪造IO结构体:
    • vtable指向_IO_str_jumps-8
    • _IO_buf_base指向"/bin/sh"
    • _s._free_buffer指向system

关键检查点:

  • 使用str_finish_check函数验证构造的字段是否通过检查

案例2:ASIS2018 fifty-dollars

特殊之处:

  1. 只能申请0x60大小的堆块
  2. 需要通过两次_chain链接实现控制:
    • _IO_list_all指向unsorted bin
    • _IO_list_all->_chain指向unsorted bin+0x68
    • _IO_list_all->_chain->_chain指向unsorted bin+0xb8(伪造的结构)

利用技巧:

  1. 通过UAF构造unsorted bin
  2. 使用fastbin attack修改chunk size
  3. 精心构造两次_chain索引实现最终控制

五、总结

  1. glibc 2.24引入的vtable check机制有效阻止了传统的vtable劫持方法
  2. 通过分析内部vtable_IO_str_jumps_IO_wstr_jumps,发现了新的利用路径
  3. 关键点在于精心构造IO FILE结构体,满足:
    • vtable指针位于合法段内
    • 通过_IO_flush_all_lockp的检查
    • 触发_IO_str_finish中的函数调用
  4. 实践中需要注意不同题目对堆块大小的限制,可能需要创造性地使用多次_chain链接

六、扩展思考

  1. 其他内部vtable是否存在类似可利用的函数?
  2. 如何结合缓冲区指针的任意读写实现更灵活的利用?
  3. 在更现代的glibc版本中,这些方法是否仍然有效?有哪些新的防护机制?
IO FILE vtable check机制及绕过方法详解 一、背景介绍 在libc 2.23及之前版本中,劫持vtable指针以及FSOP(File Stream Oriented Programming)是常见的利用方法。由于vtable包含众多功能强大的函数指针,缺乏保护机制显然不安全。因此,glibc在2.24版本引入了vtable check机制,使得传统的修改vtable指针指向可控内存的方法失效。 二、vtable check机制分析 1. 检查流程 当尝试修改vtable指针指向堆或栈地址时,glibc 2.24会报错:"Fatal error: glibc detected an invalid stdio handle"。检查机制的核心在 _IO_vtable_check 函数中: 第一层检查 : IO_validate_vtable 函数验证vtable指针是否在glibc的vtable段中 第二层检查 :如果vtable不在合法段中,则进入 _IO_vtable_check 函数 2. 检查机制总结 判断vtable地址是否在glibc的vtable数组段中 如果不在,则判断是否为外部合法vtable(重构或动态链接库中的vtable) 都不满足则报错退出 三、绕过vtable check的方法 1. 使用内部vtable: _IO_str_jumps 或 _IO_wstr_jumps _IO_str_jumps 利用原理 _IO_str_jumps 虚表中的 _IO_str_finish 函数存在可利用的代码: 利用条件: 将vtable地址覆盖为 _IO_str_jumps-8 ,使 _IO_str_finish 成为伪造的 _IO_OVERFLOW 函数 构造 fp->_IO_buf_base 不为NULL(通常设为"/bin/sh"地址) 设置 fp->_flags 最低位为0(不包含 _IO_USER_BUF ) 设置 _s._free_buffer 为 system 或one gadget地址 利用步骤 通过FSOP触发 _IO_flush_all_lockp 中的 _IO_OVERFLOW 调用 满足 _mode <= 0 且 _IO_write_ptr > _IO_write_base 伪造IO FILE结构体: vtable指针指向 _IO_str_jumps-8 _IO_buf_base 指向"/bin/sh"字符串 _flags 设为0 _s._free_buffer 设为 system 地址 最终执行: system("/bin/sh") _IO_str_jumps 定位方法 通过vtable最后地址减去0x168 或通过 _IO_wfile_jumps 地址减去0x240(对于 _IO_wstr_jumps ) 2. 使用缓冲区指针进行任意内存读写 (将在后续文章中详细讨论) 四、实践案例 案例1:hctf 2017 babyprintf 利用步骤: 使用格式化字符串泄露地址 通过堆溢出覆盖top chunk size 利用unsorted bin attack改写 _IO_list_all 伪造IO结构体: vtable指向 _IO_str_jumps-8 _IO_buf_base 指向"/bin/sh" _s._free_buffer 指向 system 关键检查点: 使用 str_finish_check 函数验证构造的字段是否通过检查 案例2:ASIS2018 fifty-dollars 特殊之处: 只能申请0x60大小的堆块 需要通过两次 _chain 链接实现控制: _IO_list_all 指向unsorted bin _IO_list_all->_chain 指向unsorted bin+0x68 _IO_list_all->_chain->_chain 指向unsorted bin+0xb8(伪造的结构) 利用技巧: 通过UAF构造unsorted bin 使用fastbin attack修改chunk size 精心构造两次 _chain 索引实现最终控制 五、总结 glibc 2.24引入的vtable check机制有效阻止了传统的vtable劫持方法 通过分析内部vtable _IO_str_jumps 和 _IO_wstr_jumps ,发现了新的利用路径 关键点在于精心构造IO FILE结构体,满足: vtable指针位于合法段内 通过 _IO_flush_all_lockp 的检查 触发 _IO_str_finish 中的函数调用 实践中需要注意不同题目对堆块大小的限制,可能需要创造性地使用多次 _chain 链接 六、扩展思考 其他内部vtable是否存在类似可利用的函数? 如何结合缓冲区指针的任意读写实现更灵活的利用? 在更现代的glibc版本中,这些方法是否仍然有效?有哪些新的防护机制?