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函数中:
-
第一层检查:
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; } -
第二层检查:如果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. 检查机制总结
- 判断vtable地址是否在glibc的vtable数组段中
- 如果不在,则判断是否为外部合法vtable(重构或动态链接库中的vtable)
- 都不满足则报错退出
三、绕过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);
}
利用条件:
- 将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地址
- vtable指针指向
-
最终执行:
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
- vtable指向
关键检查点:
- 使用
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版本中,这些方法是否仍然有效?有哪些新的防护机制?