《VScape - Assessing and Escaping Virtual Call Protections》 论文笔记

 

一、简介

这篇论文介绍了一种面向伪对象编程(COOP)的加强攻击手法,称为 COOPlus。对于那些不破坏 C++ ABI 的虚拟调用保护来说,有相当一部分的 虚拟调用保护手段易受 COOPlus 的攻击。

符合以下三个条件的虚拟函数调用容易受到 COOPlus 的攻击:

  • 不破坏虚函数调用的 ABI
  • 不保证 C++ 对象 vtable 指针的完整性(即可以被修改)
  • 允许在虚拟函数调用点上调用不同的函数

COOPlus 本质上是代码重用攻击,它在目标虚拟函数调用点上调用符合类型但不符合上下文的虚拟函数。该调用可通过 C++ 语义感知的 控制流完整性 CFI 检测,但由于调用上下文不同,因此可能会造成进一步的利用。

除了 COOPlus 以外,该论文还提出了一种解决方案 VScape,用来评估针对虚拟调用攻击保护的有效性。

论文 + 幻灯片 – USENIX security 21

 

二、虚拟调用保护

在进一步学习 COOPlus 之前,我们需要了解一下现有的虚拟调用保护手法。

由于大部分 vtable 劫持攻击都涉及到纂改 vptr,因此一种简单的方式是确保 vptr 完整性,例如通用数据流完整性技术 DFI。但通常精度不高,且运行时开销较大,不太实用。

另一种方式是破坏掉了 C++ 的 ABI,例如有些保护方法将 vptr 放入单独的 元数据表中,并利用硬件功能(例如英特尔内存保护扩展插件)来确保元数据表的完整性,防止 vptr 被纂改。由于 ABI 被破坏,因此此类的保护方式会导致较为严重的兼容问题,实用性也不大。

第三种保护方式是,检查每个虚拟调用目标的有效性。这个保护方式在之前阅读的论文 《SHARD: Fine-Grained Kernel Specialization with Context-Aware Hardening》中也用到过,通过检查 vptr 指向位置的有效性,来确认调用的虚函数是否是正确的。

对于 CFI 技术来说,其解决方案均以安全性和实用性为目标。其中对于粗粒度(即不考虑C++语义或类型信息)的CFI方式来说,无法防止虚拟函数调用攻击;而细粒度 CFI 解决方案将会考虑更多的信息来提供更强的防御。

 

三、 COOP 攻击

在说明 COOPlus 攻击之前,我们必须先说明一下 COOP 攻击,以了解 COOPlus 攻击所提出的改进点。这篇论文中对 COOP 攻击描述的不多,因此我找了一下提出 COOP 的论文,大概的看了一下。

COOP,即面向伪对象的编程。这个攻击方式在 2015 年被首次提出,直至现在其论文引用量多达三百余次。

COOP 攻击受限于篇幅,将在另一篇文章中记录。

 

四、COOPlus 攻击

COOPlus 攻击的目的是为了绕过 C++语法感知的 CFI 解决方案,因此其他漏洞缓解措施(例如 ASLR、DEP等等)以及其他漏洞利用手法等暂时不做考虑。

与 COOP 攻击不同,COOPlus 调用的是类型兼容的虚拟函数来绕过更强的防御。

COOPlus 攻击的条件是:

  • 不保证 vptr 完整性
  • 不破坏 C++ ABI
  • 存在一个低危漏洞,例如一字节越界写 off-by-one

该攻击的原理如下图所示:

一图胜过千言万语。

通俗的说,主要攻击过程概括如下:

假设有三个类,分别是基类 Base 类Base 派生类 S1另一个 Base 派生类 S2。其中 S1、S2 是否也是派生关系并不重要。只要确保 S1 类和 S2 类都是从基类 Base 类中派生出来的即可。

  • 寻找一个派生类 S1 调用 Base 基类虚函数的函数调用进行劫持
  • 利用给定的漏洞(例如一字节越界写)来修改派生类 S1 的 vptr 为 另一个 Base 类的派生类 S2 (简称 counterfeit 类) 的 vptr

    S1 类和 S2 类都是从基类 Base 类中派生出

    而对于虚函数调用来说,由于 vcall 肯定是通过基类指针进行调用,而 S1 和 S2 都是基类的派生类,因此在 C++ 语义敏感层面将通过检查。因为从基类 ptr 调用派生类虚函数是非常正常的事情,除非保护手法非常的细粒度,否则就无法检测出这类利用方式。

    个人猜测正是因为这点使得 COOPlus 可以绕过相当一部分的C++ 语义敏感的保护手法。

  • 接下来,由于 victim 类的 vptr 被修改为 counterfeit 类(伪造类),因此 victim 类的所有虚函数调用最终都将调用到 counterfeit 类的虚函数。如上图所示,当被篡改 vptr 后的 victim 类对象调用虚函数 func1 时,它将不再调用 S1::func1,而是调用 S2::func2。由于 S2 和 S1 的类布局不同,因此可能会存在一些 S1 所没有的字段(例如图中的 memberM)。

    而 S1 调用了 S2 的 func1,因此将超过 S1 类对象的内存界限进行内存访问,最终造成内存越界操作。

当 victim 类对象的函数操作可以造成内存越界后(内存越界到的对象称为中继对象 Relay object),我们便可以利用这种内存越界来精心修改 Relay object 上的字段,例如 length 等等,来进一步放大漏洞危害(最初的漏洞是一字节越界写)。

对于不同的 counterfeit 函数,大致将其分为以下几类可利用的 vfgadget:

  • Out-of-bound Read
    • Ld-Ex-PC:可以从目标内存中读取可控数据并加载进 PC
    • Ld-AW-Const:可以将常量值写入目标内存
    • Ld-AW-nonCtrl:可以将非恒定且不可控的值写入目标内存
    • Ld-AW-Ctrl:可以将可控值写入目标内存

    鉴于这四种 gadget 都分类至 OOB read,因此推测这里的 目标内存 应该指的是 victim Object 上的成员变量,或者特定其他堆空间等等。

  • Out-of-bound Write
    • St-Ptr:可以将指针值写入中继对象。若中继对象可被操作,则可以用来绕过 ASLR 等防御手段
    • St-nonPtr:将非指针值写入中继对象。例如将一个超大值写入至中继对象的 length 字段,造成更大范围的 OOB-RW。

COOPlus 攻击无需用到较为高危的漏洞,只需用到简单的低危漏洞即可放大漏洞影响,实用性较好。由于 victim 基类和 counterfeit 派生类通常都在同一个模块中定义,因此其 vtable 的分布也较为相近。漏洞对 vptr 一字节的改动也有可能产生另一个兼容 vptr,并成功利用 COOPlus。

但即便如此,若原始漏洞的效果较低,那么其 COOPlus 可用利用原语的条目数量也会降低。例如一字节越界写只能修改 vptr 正负偏移 255 字节左右,范围不够大。

 

五、VScape

1. 简介

若给定一个目标程序一个漏洞以及当前使用的虚拟调用保护方式,判断能否通过发起 COOPlus 来绕过 CFI 保护是比较艰难的,尤其是目标程序很大的时候。

这是因为若想发起 COOPlus 攻击,则需要找到适当的攻击原语元组 (vcall, victim class, counterfeit class),同时

  • 虚拟调用所调用的函数必须是基类虚函数
  • counterfeit 类和 victim 类均派生自某个基类,但却有不同的虚函数实现方式
  • 可以利用漏洞来破坏 victim 类

除此之外,我们还需要生成适当的输入,使得可以触发目标 vcall,接着触发 counterfeit 函数并最终导致内存越界操作,这整个过程同样也是一项较为艰难的任务。

因此 该论文提出 VSCape 这样的一个解决方案,用来自动编译候选的原语,并过滤出实用且可达的原语,辅助生成最终的漏洞利用来绕过 vcall 保护。

这是 VScape 的整体架构,接下来将分别在下面详细说明每个模块:

这个工具虽然在实际中我们可能不会太用到,但是了解一下整体的设计也是一个学习的过程。

2. 原语生成

Info Collecting

VScape 将使用传入的目标程序源码,在编译期间收集与 vcall 相关的信息:

  • 虚函数调用点:记录目标程序的所有虚拟函数调用点,以及预期虚拟函数静态声明的基本接口类信息。
  • 类布局:在编译过程中记录下所有类的布局,包括类大小,成员变量字段偏移量以及基类等等。
  • 虚函数信息:记录每个虚函数调用点的所有符合类型的虚函数,以及每个虚函数中的最大字段访问偏移量,以便于在今后的检查中找到潜在的越界访问。

Primitive Searching

从上一步获取到的信息中,VScape 将继续筛选出可用于攻击的攻击原语元组。

  • 首先,VScape 将构建类继承(class inheritance hierarchy ,CHI) 树
  • 初始化全局编号, 该编号用于记录目标虚拟函数(注意不是所有虚拟函数)的版本,从0开始
  • 在 CHI 树中运用 BFS,给每个类节点编号,以记录目标虚拟函数的版本。若子类使用的虚函数是父类版本,则将父类的 ID 分配给子类,否则将全局编号自增1并赋给子类。

这样操作后,VScape 就可以获得对应 vcall 的带版本号的 CHI 树。即最终可以形成可用的攻击原语 (vcall, victim class, counterfeit class)

但这里存在一个问题,由于 vcall 数量规模非常的大,而且类也很多,因此这样一套搜索可能会消耗非常长的时间,不过这还是取决于具体实现。

Primitive Capability Analysis

在有了多组攻击原语后,接下来需要判断这些原语在漏洞利用中所能起到的作用。

正如上面将 vfgadget 分成多种类型一样,VScape 在这里也将对不同类型的 vfgadget 进行不同的处理。

  • 对于OOB-read,分析读取的值用作加载 PC 还是用作写入目标内存地址。如果是后者则还会通过污染分析来判断待写入的值能否被敌手控制。
  • 对于 OOB-write,分析写入的值是否是指针,如果是则进一步查找中继对象的使用方式,来尝试找到绕过 ASLR 的地方。

3. 检测原语结构

在获取到大量攻击原语后,需要进一步过滤出可用的原语。

Vulnerability Matching

在给定漏洞描述之后,VScape 还会了解目的堆分配器的相关信息,并过滤出那些:

victim object 与 可触发漏洞的 buf 分配在同一个堆中的 候选原语。

因为若分配不在同一个堆,则自然这些攻击原语将无法利用。

Exploitable Memory States Inference

若想触发 vcall 中的特定目的(例如写入数据或读取),则必须在特定内存状态下运行,例如类的某些字段必须为某些特殊值,否则将不满足 vcall 的条件判断,进而无法执行到目标位置。

VScape 将通过污点分析和符号执行来进一步确认。VScape将把 victim object 和相邻的中继对象标记为符号值,并以符号方式执行那些会越界访问到中继对象的伪造函数。

很容易理解为什么要将中继对象也作为符号值,这是因为伪造函数可能会使用到一些越界内存上的值,而这些内存上存放的是中继对象。

4. 约束求解

在上面 VScape 已经对原语结构进行了简单的过滤,接下来仍然有三个问题需要解决:

  • 能否使控制流到达目标 vcallsite 上并执行 victim 类的 vcall。
  • 伪造函数上的 OOB 操作能否成功执行
  • 满足上述两点的数据约束是什么

Virtual Callsite Reachability Testing

首先对于第一点,VScape 通过定向 fuzz 技术,使用给定的基准测试数据,尽可能地得到一个不完整的可达 victim 函数列表。VScape 将在目标 vcallsite 后插入 callback 以记录调用的 victim function 和 testcase。

OOB Instruction Reachability Solving

对于第二点,VScape 把经过上面第一点处理后的 testcase 作为输入,在目标程序执行至目标 vcallsite 后保存此时的执行上下文,并让符号执行引擎在此时的上下文对伪函数进行符号执行操作,以获取执行伪函数 OOB 操作的数据依赖。

类似的,中转对象也会被作为符号值一并用于符号执行中。

Exploit Assembling

VScape 无法自动化生成漏洞利用,它必须依赖用户给定的 exploit 模板来构成完整的漏洞利用链。

用户必须手动:

  • 在 exploit 中手动操作堆风水
  • 在 exploit 中,利用 POC 更改 victim object 的 vptr 为特定值
  • 根据 VScape 提供的信息进行后续的漏洞利用

以这个漏洞模板为例:

main 函数中的黑色字体函数调用是必须由人工手动完成,而红色字体的函数调用是 VScape 可以辅助完成的工作。

 

六、评估

VScape 的评估主要基于三个层面:

  • 在真实世界中的 C++ 程序中,COOPlus 攻击是否实用
  • COOPlus 在绕过 vcall 保护机制上效果如何
  • VScape 在生成真实完整漏洞利用链的过程中表现如何

根据 slides 中给定的结论,我们可以看到 COOPlus 攻击在大项目中比较实用。

而对于那些 vcall 保护机制,COOPlus 可以绕过满足既定攻击条件的保护。

既定攻击条件,即不破坏C++ ABI,不保证 vptr 完整性以及允许在 vcallsite 上调用多个目标。

论文中还给出了对于 PyQt 和 Firefox 的利用评估,这里不再展开。

(完)