glibc2.24下的vtable check以及绕过
IO_FILE知识
[toc]
glibc2.24下的vtable check以及绕过
vtable的check
在glibc 2.23中,我们可以劫持_IO_FILE_plus中的vtable,并使其指向我们可控的内存区域,便可使用FSOP等方式调用我们所需的函数。
然而,在glibc2.24下就有了关于vtable劫持的check。例如,我们可以在glibc2.23下使用如下代码完成vtable的劫持,触发后门函数:
1 | |
上面的代码通过正常退出程序,程序使用_IO_flush_all_lockp() -> _IO_new_file_overflow()方式情况调用_IO_FILE_plus中的_IO_overflow函数来清空缓冲区。由于我们劫持了vtable并覆盖了_IO_overflow函数为后门函数,因此可以触发后门,效果如下所示:

然而,上面这段代码在glibc2.24下完全不可行,并且抛出Fatal error: glibc detected an invalid stdio handle错误:

这是因为在glibc2.24中新增了对vtable的安全检查:
1 | |
上面的代码可能看起来有些吃力,但是在了解到下面的知识后就会轻松很多:
在
glibc中,存在多种vtables,用于不同场景下,例如_IO_FILE_plus中的_IO_file_jumps虚表就用于文件操作这些虚表都位于
__stop___libc_IO_vtables以及__start___libc_IO_vtables两个变量之间比如有
_IO_file_jumps虚表、_IO_str_jumps虚表
根据以上知识,我们可以得知,上面的代码将会校验_IO_FILE_plus的虚表是否位于存放虚表的那一片空间内,若不位于存放虚表的那片空间,则会进一步通过_IO_vtable_check()函数进行校验,而该函数较难进行绕过,因此我们在glibc2.23下已经无法通过以前的方式对vtable进行劫持了。
柳暗花明
我们上面提到:
在glibc中,存在多种vtables,用于不同场景下,例如_IO_FILE_plus中的_IO_file_jumps虚表就用于文件操作
那么,虽然我们无法像以前一样劫持vtable到可控的堆空间,但我们可以劫持_IO_file_jumps为其他的虚表,例如_IO_str_jumps虚表。
劫持为其他虚表后,我们可以利用逻辑上的一些问题进行攻击。
新的利用链 _IO_flush_all_lockp -> _IO_str_finish(<=glibc2.27可用)
我们上面已知可以合法的将_IO_FILE_plus的vtable劫持为_IO_str_jumps虚表。那这有什么作用呢?
在_IO_str_jumps虚表中,有一个函数叫做_IO_str_finish:
1 | |
其中有一处非常关键:
1 | |
可以看到_IO_str_finish函数中,通过_IO_FILE_plus指针fp的偏移来执行了一个函数。为什么我们要劫持vtable而不是直接修改vtable里面的函数?是因为vtable是不可写的!而在_IO_str_finish函数中,可以通过fp的偏移来执行函数,而我们知道fp即结构体_IO_FILE_plus是完全可写的。因此,只要我们劫持_IO_FILE_plus的vtable为_IO_str_jumps,并将fp对应偏移处修改为system函数的指针,那么就可以通过下面的函数调用链(正常退出、exit、abort)来执行任意函数了:
1 | |
其实有的师傅可能会问,函数调用链不是_IO_flush_all_lockp() -> _IO_new_file_overflow()吗?如何才能执行到_IO_str_finish函数呢?
在_IO_str_jumps表中,有如下函数:
1 | |
若我们按照正常情况调用_IO_FILE_plus中的_IO_overflow函数,那么偏移是vtable[3],对应到IO_str_jumps中就是_overflow函数。而我们刚刚提到,这些虚表在内存空间是完全连续的,如图所示:

因此,我们只需要将_IO_FILE_plus的vtable的值覆盖为_IO_str_jumps - 8 ,即可让vtable[3]指向_IO_str_finish函数,由此一来,我们以往的函数调用链_IO_flush_all_lockp() -> _IO_new_file_overflow()即可变为_IO_flush_all_lockp() -> _IO_str_finish()。
再来看如何修改fp指针对应偏移的函数,主要有这么两行:
1 | |
首先第一行,要fp->_flags & _IO_USER_BUF不为0才可以。而_IO_USER_BUF实际上是一个宏,其定义为#define _IO_USER_BUF 1,因此只需要其fp->_flags,也就是偏移为0处的值的最低位为0即可。对于fp->_IO_buf_base,实际上是接下来要执行的函数的参数,我们要控制其不为0即可。
第二行,执行的函数看起来十分奇怪,其首先使用(_IO_strfile*)将fp进行变量类型的强制转换,然后再执行转换后的fp结构体指针指向的_s的_free_buffer存放的函数。我们需要控制_free_buffer中存放的函数才可以。实际上,我们知道C语言中结构体被解释为什么不重要,它对应的偏移才重要,那么我们在gdb中查看到((_IO_strfile *) fp)->_s._free_buffer对应fp起始处的偏移,然后将其覆盖为system即可。如图所示:
若我们将fp解释为(_IO_FILE_plus*),那么fp为:

将其解释为_IO_strfile *,那么其会变为:

可以看到,实际上内存中的值不会发生变化,只是看如何对其进行解释。那么我们查看((_IO_strfile *) fp)->_s._free_buffer的偏移如下:

即覆盖对应fp偏移为0xe8处的函数为system,覆盖fp->_IO_buf_base处的值为/bin/sh\x00的地址即可执行system('/bin/sh')。
以C语言手写一个glibc2.24下的vtable check绕过如下:
1 | |
总结一下函数调用链_IO_flush_all_lockp() -> _IO_str_finish() -> system()需要满足的条件:
fp -> _IO_write_ptr大于fp -> _IO_write_base,分别对应fp偏移0x20和0x28(这是_IO_flush_all_lockp()要满足的条件)fp -> _flag最低位为0,偏移为0x0- 设置
vtable为_IO_str_jumps - 0x8,定位_IO_str_jumps可以通过_IO_file_jumps等虚表定位。 fp -> _IO_buf_base存放要执行函数的参数的地址,偏移为0x38(_IO_strfile* )fp -> _s._free_buffer存放要执行的函数,对应偏移为0xe8
另一条调用链 _IO_flush_all_lockp -> _IO_str_overflow(<=glibc2.27可用)
原理和上面的利用链是一样的,我们此处不再详细阐述,仅仅写下需要构造的条件来供查阅。
fp -> _flag最低两字节为0。其偏移为0。fp -> _vtable指向_IO_str_jumps。_vtable偏移为0xd8- 偏移
0xe0处为要执行的函数,例如system fp -> _IO_buf_base为0,其偏移为0x38fp -> _IO_buf_end为(bin_sh_addr - 100) / 2,其偏移为0x40。其中bin_sh_addr是函数参数的地址,若为奇数需要+1fp -> _IO_write_base为0,其偏移为0x20fp -> _IO_write_ptr为0,其偏移为(bin_sh_addr - 100) / 2 + 1
上面是通常情况下可以调用函数的参数设置,也可以看下面的C语言实现,其中注释包含了详细的要求:
1 | |
后记
绕过vtable check的方法除了_IO_str_jumps虚表,_IO_wstr_jumps虚表也是同样的。_IO_wstr_jumps和_IO_str_jumps功能基本一致,只是_IO_wstr_jumps是处理wchar的。
上面提到了这些vtable check的绕过方法都只是在glibc2.27及以下可用,因为到了glibc2.28中,_IO_strfile中的_allocate_buffer和_free_buffer已经被简单粗暴地用malloc和free来替换了,自然也就没有函数指针用于覆盖。
参考链接: