《Counterfeit Object-oriented Programming》 论文笔记

一、简介

现阶段,ROP (面向返回的编程技术) 已经成为了一种非常流行的利用手法,同时现在也存在各种方式来保护程序免受 ROP 工具,例如 shadow stack 技术。而这篇 2015 年的论文向我们展示了一种新的利用手法,称为面向伪对象编程(COOP),即只通过程序中现有虚函数链以及 callsite 来进行恶意攻击。该攻击方式是图灵完备的,即可以执行任何操作,包括条件分支等。

同时,COOP 技术也可以绕过那些 不精确考虑C++面向对象语义 的防御手段。它并不针对与某一类语言(例如 C++),因此自然无法防护 COOP 技术。

该攻击手法基于 C++ 虚函数的一些特性:

  • C++ 编译器通过 vtable 虚函数表来实现对 vcall 虚函数 的访问

    其中 vtable 是指向类的所有可能继承继承的虚函数的指针数组。

    根据逆向结果来看, vtable 通常位于 .rodata 段上。

  • 对于包含虚函数的类来说,其对象内存开始处(即偏移量为0)包含一个指向 vtable 的指针。

二、面向伪对象的编程

1. 目标

对于常规代码重用攻击,例如 ROP,其攻击手法包含一项或多项特性:

  • C-1:间接调用或跳转至非 address-taken 的位置。

    非 address-taken位置,个人认为应该是那些不使用函数指针而执行到的代码位置,例如各类 gadgets。

    即虚函数是 address-taken 的。

  • C-2:从函数返回时,不符合调用堆栈

  • C-3:过度使用间接分支

  • C-4:劫持堆栈指针

  • C-5:注入新代码或者操作现有代码

由于常规的攻击包含了这些特性,因此这类攻击,将会被那些低级且与语言无关的保护手法所检测出。

故 COOP 所定义的目标如下所示:

  • G-1:不得暴露特性 C-1 至 C-5
  • G-2:务必展示出类似于正常 C++ 代码执行的控制流和数据流
  • G-3:广泛应用于 C++ 应用
  • G-4:实现图灵完备

2. 敌手模型

COOP 攻击的实施,要求攻击者

  • 可以劫持一个 使用 vptr 的 C++ 对象(即劫持一个存在虚函数的对象),并能推断出该对象的基地址,或者可以控制足够大小的缓冲区。
  • 能推断出一组 C++ 模块的基地址,并且了解该模块的二进制布局。

3. 基本攻击方法

在说明攻击方法之前,先给出以下几个定义

  • initial object (下称初始对象):目标程序中被劫持的 C++ 对象,一切攻击从这里开始。

  • counterfeit objects(下称伪造对象): 携带攻击者所选择的 vptr 和一些精心构建的数据字段,并被攻击者批量注入进可控内存中。正如名字所示,这个“对象“是攻击者手动伪造的。

  • Vfgadgets:COOP 攻击中将会使用到的虚函数。vfgadgets 的类型如下表所示:

    image-20211202111754544

大体的定义已经在上面给出,接下来将详细说明攻击方式:

  1. 首先,为了重复调用虚函数,COOP 攻击需要依赖 ML-G 类型的 vfgadget(即上面列表中的第一个条目)

    ML-G:可以理解成攻击的事件循环。它将遍历一个指向伪造对象的指针数组,并依次调用其中的虚函数

    这类 vfgadgets 在 C++ 应用程序中非常常见。

    如该图所示,图中的 Course::~Course 虚函数,即为ML-G 类型的 vfgadget。

    image-20211202161904967

  2. 接下来,攻击者将初始对象的内存,布局为类似 ML-G 的类的对象(例子中的目标对象为 Course)。

    其中,初始对象中的 vptr,被攻击者修改为 Course 类的原始 vptr 相对偏移一点的地址。这是为了使得初始对象接下来的第一个虚函数调用,可以调用至目标的虚函数(即可调用至 ML-G vfgadgets)。

    注意,图中左边那块内存,是攻击者完全可控的。即攻击者在可控的内存内构建了一个完整的 Course 类对象,包括其内部成员的指针数组。

    同时,字段 stutdents 指针数组所指向的各个 object,即为伪造对象,其 vptr 均可控。

    还需要注意的是,由于伪造对象是攻击者自己伪造的,因此实际上伪造对象可以不是同一种类型,例如一种伪造对象是 string 类型,另一种伪造对象是 Student 类型。

    image-20211202162256571

  3. 修改伪造对象的 vptr。由于伪造对象在被 ML-G 调用时,其调用目标可能不是攻击者所期望的 vfgadgets(例如Fig 1 中调用的是 Student::decCourseCount 函数),因此攻击者需要修改伪造对象的 vptr 指针,使得当伪造对象在虚函数调用点被调用时,可以调用到目标 vfgadget

    这里的修改可以从原先的 vtable 地址(例如 Student 的 vtable 地址)相对的前后偏移一点位置,使得此时的 vptr 指针指向了原先 vtable 地址向后一点的函数位置。

    当上述三个步骤完成后,我们便可以通过操纵 伪造对象的 vptr,搭配 ML-G 类型的 vfgadget,来进行任意数量的 vfgadgets 调用

    image-20211202164733622

  4. 覆盖伪造对象。先上两张图,首先是给出的两个目标类的内存布局以及其目的 vfgadgets:

    image-20211202165553242

    这里会用到两个 vfgadgets,分别是:

    • Exam 类中的 ARITH-G(算数或逻辑操作):注意到该函数会将三个成员变量的和,写入至当前类对象中的另一个字段
    • SimpleString 类中的 W-G(写入数据至目标地址):注意到该函数会使用当前类对象的某个字段,作为复制操作的 length。

    注意到上面标注的粗体内容,一个是写入数据,一个是读取数据。因此攻击者可以精心将两个对象的内存重叠,使得 W-G 中使用的 length 刚好是 ARITH-G 所计算出的结果,这样就可以造成越界写入。
    以下是构建的内存布局,注意 ARITH-G gadget 会把计算出的结果写入至 SimpleString 类型的 len 字段中

    image-20211202170110810

    此时可能会有疑问,这里的 SimpleString 和 Exam 类会在哪里被使用呢?

    实际上,攻击者将会精心构建这两个类的类对象,以作为 ML-G 类中的 伪造对象,被 ML-G vfgadget 调用其虚函数。

综上,基本的攻击方法如上所示。其攻击过程可以看成 单个 vcall -> 多个 vcall -> OOB。需要注意的是,基本攻击手法没能传递任何参数vfgadget

4. vfgadget 参数传递

参数传递的方式,取决于函数调用约定。

a. 通过多个寄存器传递调用参数

  1. 首先,挑选一个合适的 vfgadget 并执行,以便于将伪造的字段分别写入至函数调用参数传递寄存器
  2. 执行目标 vfgadget,其参数使用上一个 vfgadget 刚刚伪造的值。

该操作要求 ML-G 不修改参数传递寄存器(包括不能传递参数给 vfgadget)。

b. 通过单个寄存器 + 栈传递调用参数

例如 thiscall 调用约定,this 指针通过 ecx 寄存器传递,其他参数通过栈传递。

该情况的参数传递依赖于 ML-G 主循环。ML-G 应该将初始对象的某个字段作为参数(将传递参数的 ML-G 称为 ML-ARG-G)传入给每个 vfgadget。之后,攻击者可以使用以下方法来将目的参数传递给目标 vfgadget:

  1. 传递的参数是一个指针,指向一个临时可写内存。这样 vfgadgets 可以通过读写这块内存来传递参数,例如以下示例:

    image-20211202205055482

    图中 ML-G Course2::~Course2 vfgadget 传递了一个参数id 给其他 vfgadget。而在Student2::getLatestExam 方法中,实际上将参数视为一个指针,因为引用的本质是指针。在该函数中,控制流动态的修改了参数所指向的内存。这样当下一个 vfgadget 获取到参数后,它便能读取上一个 vfgadget 所保留的信息

  2. 动态重写参数。论文里说明该方法允许攻击者将任意参数传递给 vfgadgets,但该方法需要一个可用的 W-G 类型的 vfgadget。

    该方法暂时存疑,因为私以为该方法和第一个方法有异曲同工之处。

    参数传递正常来说是按值传递,因此按理来说重写本地参数副本将无法影响到其他 vfgadgets 所获取到的参数值。

    而在这类 vfgadget 中,单独使用某个字段的较为少见,因此参数传递大多还是使用第一个方法。

c. 传递多个参数

先上张图:

image-20211202210233297

  • 若调用的 vfgadget 实际使用的参数个数比 ML-ARG-G 传递的要时,新传递的参数将被永久压栈(因为不使用参数的 vfgadget 将不会清理栈)。多调用几次(即多压几次栈),那么栈内存上就有构建好的一组参数值。
  • 而若调用的 vfgadget 实际使用的参数个数比 ML-ARG-G 传递的要时,在 vfgadget 函数返回时,该函数将额外弹出”参数“的栈空间,即向下恢复栈。

5. API 函数的调用

COOP 攻击可以使用以下三种方法尝试调用 WinAPI 函数:

  1. 使用一个正常调用 WinAPI 的 vfgadget。

    缺点:大多情况下不可行。

  2. 在 ML-G 中像调用 vfgadget 一样调用 WinAPI。

    优点:易于实现。例如让 vptr 指向诸如 GOT、IAT、EAT 等表。

    缺点:

    1. 违反目标G-2,没能展示出类似于正常 C++ 代码执行的控制流和数据流
    2. 受限于调用约定,伪造对象的指针总是以第一个参数传递给 WinAPI。
    3. 使用一个调用 C 风格函数指针的 vfgagdet。该方法需要使用一种特殊 vfgadget:INV-G。例如下图中的 vfgadget:

    image-20211202211308760

6. 实现分支和跳转

COOP 攻击是图灵完备的,因此这里需要说明一下 COOP 攻击如何实现分支和跳转功能。

注意到 ML-G 使用索引来遍历伪造对象。(例如 for 循环上的 int 类型索引,或者容器迭代器)。

COOP 攻击可以通过使用 W-COND-G vfgadget 来在满足某些条件的情况下,重写 ML-G 的索引,或者修改下一个待遍历的伪对象的指针

这种重写需要知道对应变量的地址,若索引存放在栈上,则可以通过上面的压栈和弹栈来移动栈指针,达到修改目标地址上索引的目的。

image-20211202215512956

三、防护手法

以下几种方式可以防止或缓解 COOP 攻击:

  • 通用防护技术

    1. 限制合法API的 callsite。但可能较难精确识别给定 API 函数模块的合法 callsite,而且即便限制 API 调用,COOP 仍然可以泄露一些敏感数据。
    2. 监视栈指针是否发生异常。例如在 32 位下 COOP 准备参数期间,栈指针将异常抬高,但这类检测在 cdel 调用约定中,较难将恶意行为和正常行为区分开。
  • C++语义敏感技术

    1. 验证 vptr 是否指向合法的 vtable。缺点是开销可能较大。
    2. 监视数据流。开销可能也比较大。
    3. C++ 数据结构的细粒度随机化。例如在 C++ 对象内部字段间插入随机大小的填充,或者对 vtable 位置和结构进行细粒度随机化。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2022 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~