Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

PTAuth: Temporal Memory Safety via Robust Points-to Authentication

作者:Reza Mirzazade farkhani, Mansour Ahmadi, Long Lu

单位:Northeastern University

会议:USENIX Security 2021

原文:PTAuth: Temporal Memory Safety via Robust Points-to Authentication

Abstract

本文讲述的是运行在 ARMv8.3 下的一个内存防护机制,用于在运行时检测时序内存漏洞。所谓时序内存漏洞,在论文中特指 double free, use after free 以及 invalid free 漏洞。关键的思想在于将内存对象与指向内存对象的指针通过某种特定的方式绑定起来,并在解引用指针检测指针是否合法。在 evaluation 部分,作者用 juliet 测试集以及 SPEC CPU2006 benchmarks 测试工具的安全性以及运行时的效率,并将其与其它几款类似的工具进行了比较。

顺序

  1. 设计原理
  2. 平台相关背景知识
  3. 设计细节
  4. 安全性分析
  5. 性能测试

Introduction

在64应用层程序中,实际的被用到地址长度只有 48bit。因此剩下的高 16bit 多余的空间就可以用来存放一些的信息。而本工具就是利用这多余的 16bit 的空间存放一些 metadata 以验证指针的合法性。

这里的 AC (authentication code)用于验证指针的合法性。

那么 AC 又是怎么生成的呢?要回答这个问题,我们就得先将注意力放在这个工具的目的上来。如前面所说,这是一款针对堆内存漏洞的防护机制,所以显然 AC 的生成与堆对象脱离不了关系。所以下面我们简单看看堆块在 PTAuth 防护下长什么样子。

上图是 PTAuth 防护通过 malloc 等堆分配函数创建的一个堆块。下面的 object 是普通的 malloc 函数创建出来的堆内存数据。与普通的堆对象不同的是,在堆的上方额外存放了一个 ID(8字节)。在每次调用内存分配函数 (malloc等),都会在堆块的开头位置放置一个全局唯一的 ID,用以标记不同的堆块。

现在我们既了解了 AC 存放的位置,又了解了堆的内存布局,接下来我们就可以来说说 AC 的生成方式了。

AC 实际上由上方一个简单的指令生成。PACIA可以简单理解成一个加密函数,明文是堆的地址以及堆的ID,生成的密文即是 AC。整个工具其实可以分为三个步骤。

  1. 创建堆时,将会根据堆的地址以及 ID 通过加密运算生成 AC,并将 AC 存放在指针的高 16bit 位置。
  2. 释放堆时,将堆的 ID 设置为0。
  3. 解引用堆指针时,将会重新计算 AC ,将计算出来的 AC 与指针的高 16bit 做对比,如果两者不相等,程序终止运行。所以如果试图访问一个已经被释放过的堆块,因为 ID 在堆块释放时被修改,所以计算出来的 ID 与储存在指针高 16bit 的值将会不相等。

现在大家应该对这个防护工具有了大概的了解了。但是还有很多重要的地方没有被提及,比如密钥存放在哪?是不是对所有的解引用都需要验证?如何处理指针的传递?如果指针不是指向堆块的开头,而是指向堆块的中间又该怎么办……

不用急,针对上面这些问题,下面都会有相应的讲解。 首先说明加密算法与密钥生成的问题。

Hardware Suport

实际上,上面所说的加密流程并不是软件生成的,而是有现成的硬件支持。在 ARMv8.3-A 以及后续的版本中,存在叫做 (PAC)Pointer Authentication Code 的一个硬件功能。在 ARMv8.3-A 中,新增了几条指令

PAC 的主要功能 1. 为特定的指针生成一个签名 2. 在指针被使用之前验证该签名

上述表格中前4条即是签名指令,下面4条是验签指令。PTAuth 就是使用上面的指令来进行密钥创建与验证。

PAC 使用的是 QARMA 算法,一个轻量级的分组密码加密算法。 而密钥只能在内核态设置,用户态程序无法得知密钥的值。

Design

System Overview

下面我们首先看看整个系统的设计。首先左边的是编译时支持。 1. 将 c 语言用 clang 编译成 IR。 2. 开发了一个函数库,提供 AC 的创建以及验证等功能。 3. 开发了一个 PASS,在 IR 层面上 hook malloc/free 等关键函数以及部分的 load 和 store 指令,调用函数库里面的代码。 4. 将修改后的 IR 以及库函数链接在一起,生成最终的 binary

而右边的即是运行时的状态,也就是我们在之前的章节主要讨论的内容

Compiler-based Code Instrumentation & Runtime AC Checking

下面较为细节地讲解整个签名创建以及签名验证的流程。

AC Generation

PTAuth 针对 ptmalloc 分配器,对所有的堆创建函数 malloc/ calloc/ realloc 等函数都进行了修改。也就是对分配出来的对象前面加个 8 字节大小的 ID ,并且用上面图中的指令生成 AC ,并将其放入地址的高 16bit 位置

AC Checking

关键的步骤就在于要如何去检测一个指针是否合法。在讨论如何检测一个指针的合法性之前,我们先讨论一些需要注意的问题,首先是如何将一个指针变量传递给另外一个指针变量。

指针传递分为两种情况。 1. 第一种就是直接将一个指针变量 copy 到另外一个变量,这种情况只需要将指针的值以及高 16 bit的 AC 值直接传递到另外一个变量即可,所以实际上并不需要额外的处理。 2. 第二种情况是指针的运算,这种情况其实也不需要额外的处理,因为指针变量运算只会更改指针的低位,高16bit不会有影响,所以也是不需要额外的处理。

其次是什么时候需要对解引用语句进行检测。最简单的做法是对所有 load/store 语句都做检测,但是如果这么做的话,会带来很高的开销,所以文章作者提出了一个比较巧妙地优化方式。

  1. 对于所有的函数调用语句,如果将一个指针作为参数传入,那么在调用函数之前就需要先检测该指针是否合法。
  2. 在 1 的基础上,我们可以认为每个指针变量在刚刚被传入一个函数的时候,都是一个合法的指针。只有当这个指针变量又被传递给了另外一个函数,才认为该指针有可能不合法。
  3. 基于上述的观察,在每个函数内,仅当一个指针作为参数被传入另外一个函数之后,才需要对该指针进行检测。

举上面的例子来说,参数是 reg 指针变量,而 reg 指针变量在第13行作为参数传给了 quantum_add_hash 函数。因此在第13行之前,对于 reg 指针的解引用都不需要验证 AC 的值。

这种做法大大减少了程序运行的开销。不过为了实现这种做法,需要在编译时做过程内的指针分析,才能够精确地判断指针的指向关系,所以编译时会带来一些额外的开销。

还有一点比较关键的就是要如何区分栈指针和堆指针。论文中没有提,我猜想可能是运行时获取堆区的地址,判断指针指向的地址是否在堆区以区分堆地址与栈地址,并仅对堆指针做检测。

接下来就是检测的方式 如果指针刚好指向堆的起始地址的话,那么直接通过指针以及 ID 重新计算 AC 值,并将其与指针高位的 AC 值进行对比。

但是假如指针是通过运算之后,指向的并不是堆的起始地址,而是指向堆块的中间,这种情况下就比较复杂。

除此之外,还需要解决指针运算的问题,因为假如一个指针参与了运算,那么指针指向的可能就不是堆块的开头位置,而是中间。为了解决这个问题,作者想出的办法是不断往前搜索。假如现在指向的是堆的中间,那么验证的时候是会失败的,这种情况下就有两种处理方式

  1. 需要往前面的地址继续搜索,再次计算 AC 值并匹配
  2. 当遇到了非法的地址或者说搜索的次数超过了一个固定的阈值,那么则认为匹配失败,程序报错

我个人的想法是这个做法不太好。因为假如一个堆块很大,假如一个指针恰好指向堆的末尾,那么就需要搜索很多次,会带了非常大的开销。其次,因为搜索的次数是有上限的,有可能该合法指针因为搜索次数过多被判为非法指针,就带来误报了。

Security Analysis

Threat Model

为了说明这个工具的安全性,作者假设攻击者有如下的能力 1. 任意地址读 2. 能够通过别的攻击修改堆的ID

作者在安全性分析部分,则主要在论述即使有上面的这些攻击能力,攻击者也无法利用 double free/ use after free/ invalid free 漏洞实现进一步的攻击。

接下来文章提出了一些攻击方式,然后再说明这个工具能够抵御这些攻击

  1. 直接伪造 AC : 这种方式不可行,因为密钥无法得知。

  2. 通过劫持控制流重用 AC 生成代码: 作者认为这种方式不可行,因为开启了别的防护,无法劫持控制流。我感觉这个讨论有点多余,既然能劫持控制流了,那还不如直接调用一些危险的函数。

  3. 当指针指向堆的中间时,在堆的上方伪造大量的 ID,以在后向遍历的时候爆破 ID 值绕过防护:作者认为这个不可行,因为 ID 是和地址绑定在一起的,所以伪造 ID 没有任何作用。但是我认为作者忽视了一点:因为AC 值只有16bit ,所以攻击者是完全可以通过多次运行程序以进行爆破攻击。本来需要运行大约2^16次程序才能够实现攻击,但是因为后向遍历这一机制的存在,每次爆破失败还会继续自动往后爆破,那么实际如果想要通过爆破绕过这一防御机制,运行程序的次数还要更少。所以实际上并没有想象中这么安全。

Evaluation

在 Evaluation 部分,作者针对两个方向进行测试

Security Evaluation

在这一部分的测试中,作者运行的环境是 ARM FVP simulator。之所以不用真机,是因为到目前为止,还没有一个公开的开发板支持 ARMv8.3,所以只能用虚拟机进行测试。

测试分为两部分

Juliet 测试集

作者对测试集中的3个漏洞类型,各选取了五十个程序。然后结果表明,PTAuth能够检测这150个程序中的所有漏洞,所以准确率是100%。不过 Juliet 测试集对于每个漏洞都有数百个程序,作者好像没有提及选取这50个程序的原因。

Case Study

针对4个 CVE ,作者也用这个工具进行了测试,结果表明针对这些漏洞,PTAuth能够完全检测。

Performance Analysis

这部分的测试,作者采用的测试集是 SPEC CPU2006 benchmarks。不过这次测试并没有用 FVP 模拟器,因为模拟器的运行速度太慢了,以至于在运行 benchmarks 的时候会宕机,没法进行测试。可是目前也没有支持 PAC 指令集的开发板,所以作者用了一种取巧的方式进行测试,用软件来模拟 PAC 指令,但是软件运行肯定是比硬件要慢,所以模拟的 PAC 指令换了一种更简单的算法,使其运行效率与 PAC 指令效率接近。运行的平台是 Raspberry Pi4 (ARMv8-A Cortex A53)

Runtime Overhead

首先从这个图上面看,PTAuth 并没有比别的工具快,实际还更慢了。但作者认为,PTAuth 因为对 metadata 进行了加密,所以能够抵抗对 metadata 的溢出攻击,而别的工具不可以。为了公平起见,需要对其它的工具加上 SoftBound 工具提供更充分的保护,再进行效率的对比。

在这种情况下进行测试,对于上图中的3个 benchmark ,PTAuth 在其中两个测试中都比别的工具的性能开销要更小。

Memory Overhead

在性能开销这方面,Ptauth 是比别的工具要好不少的

总结来说的话,加了 PAauth 之后,运行时开销增长了26%,内存开销增长了2%。