这篇文章是参考UC Berkeley张超博士的一个视频和两篇NDSS论文,学习的一些关于C++虚函数安全相关的内容。虚函数调用
是面向对象中的一个很重要的特性,用来支持多态。运行时实际调用的函数是根据对象来决定的。运行时找虚函数是通过vtable
函数指针来实现,vtable指针由编译器添加到对象中。
虚函数表劫持攻击
vtable是从对象中读出来的,而对象是位于可写内存段,所以攻击者有可能篡改vtable指针。
vtable注入
伪造vtable,在每个表项中填写ROP chain的地址。利用uaf等漏洞篡改vtable指针指向伪造的vtable,虚函数调用时,触发ROP。
vtable重用
不伪造vtable,篡改vtable指针指向已有的vtable。实际中非常少。
vtable破坏
直接篡改vtable,但现在不可行,因为编译器将vtable放到只读内存页。
虚函数表防护
VTint: Protecting Virutal Function Tables’ Integrity
目标:二进制层面进行的防护,防护vtable劫持攻击。
设计:vtable注入
和vtable破坏
两种攻击方法的vtable都是可写的,因此在运行时检查vtable是否可写,如果可写则判断为受到攻击。vtable重用
可以通过区分数据和vtable进行部分防护。
实现:理解二进制程序的基础上,对二进制程序进行修改。
1.解析二进制文件,恢复高级信息,如识别vtable信息、虚函数调用点。
PE文件格式:重定位表、导入/导出表
输出:候选函数入口点(重定位表中恢复)、候选vtable(vtable是一个数组,其中每个表项是一个虚函数指针,vtable需要重定位,每个表项也需要重定位)。
2.反汇编恢复控制流图等高级信息。
(1)识别构造函数:给对象分配内存、将vtable拷贝到分配内存中、成员变量初始化。
(2)识别真正的vtable:vtable一定会在某个构造函数中被引用到,所以被引用到的就是真正的vtable。
(3)识别虚函数调用:
虚函数调用是间接调用,this指针是虚函数调用的第一个参数。
1 | mov eax, [edi] ; edi保存vtable |
3.重写二机制文件,部署安全策略。
将识别出的vtable拷贝到新的只读内存页,加一个特殊的ID VTID
来区分这个内存页。
在虚函数调用点之前添加检查语句:检查vtable的内存页是否包含VTID(区分vtable与数据);检查vtable的内存页是不是只读。
VTrust: Regaining Trust on Virtual Calls
目标:源代码层次上进行防护
设计:两层防护措施相结合。
1.验证vtable目标虚函数是否有匹配的类型
虚函数调用点处知道目标函数的函数名和参数列表,虚函数定义点处也是知道函数名和参数列表的。C++调用约定,必须是兼容类型信息:调用点处的类必须是定义点处类本身或其子类,因此直接验证这两个类型是否相等。验证方法,将类型信息编码成整数:
1 | signature = hash(funcName, paramList, qualifiers, classinfo) |
无法防护动态代码,攻击者可能伪造signature。
2.确保vtable指针是一个合法的指针
限制运行时目标虚函数必须指向静态代码,不能指向动态代码。打断传统的vtable查找过程,在中间引入一个只读内存可信区域(转换表),将合法的vtable指针放到这个转换表中,将原来存放vtable指针的位置换成一个索引,索引用来在转换表中找vtable指针。
reference
围绕虚函数调用的攻防战
VTint: Protecting Virutal Function Tables’ Integrity
VTrust: Regaining Trust on Virtual Calls