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
,其偏移为0x38
fp -> _IO_buf_end
为(bin_sh_addr - 100) / 2
,其偏移为0x40
。其中bin_sh_addr
是函数参数的地址,若为奇数需要+1
fp -> _IO_write_base
为0
,其偏移为0x20
fp -> _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
来替换了,自然也就没有函数指针用于覆盖。
参考链接: