Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

ConfLLVM: A Compiler for Enforcing Data Confidentiality in Low-Level Code

作者:Ajay Brahmakshatriya, Piyus Kedia, Derrick P. McKee, Deepak Garg, Akash Lal, Aseem Rastogi, Hamed Nemati, Anmol Panda, Pratik Bhatu

单位:MIT, IIIT Delhi, Purdue University, MPI-SWS, Microsoft Research, Saarland University, AMD

出处:EuroSys 2019

资料:Paper

1. 论文介绍

现实世界里的很多程序都不可避免地会含有很多私有数据。程序里存在的Bug和漏洞都可能导致这些私有数据被泄露出来,然后被攻击者获得(最典型的案例当然是OpenSSL Heart-bleed,可以泄露出Web服务器的私钥)。

对于这个问题,最标准的解决方法是使用静态数据流分析以及运行时污点分析来追踪私有数据。这种方案在Java和ML上效果都很好,但是在C这种不是内存安全的语言就还是很困难(原因有两点,一个追踪污点的性能开销很高,另一点是静态数据流分析没办法保证数据保密性,因为攻击者可以利用漏洞进行攻击)。

本文中作者为了解决这个问题,基于编译器设计了一套完整的实用方案。在这套方案里,程序员可以用private这样的标记在源码里标注。然后编译器就会结合静态数据流分析、运行时插桩、以及一个新式的Taint-aware CFI来保护数据在被攻击的情况下仍然不被泄露。为了减少运行时开销,编译器需要采用使用一些新的内存布局。

作者在LLVM上实现了这套方案(ConfLLVM),并且用标准SPEC-CPU Benchmard以及现实世界中的大型应用程序(例如NGINX、OpenLDAP)进行了评估,最后实验结果表明,插桩带来的运行时开销是是可以接受的(SPEC上12%),程序员需要对应用程序源码进行的移植也是很小的。

## 2. 方案设计

2.1 威胁模型

考虑一个C程序,程序里有私有和公开数据,程序需要与外部世界进行交互,例如通过网络、磁盘、以及其他的一些信道。攻击者可能通过与程序交互,发送攻击流量,本文中作者的目标就是为了阻止攻击者通过交互泄露出数据。

2.2 Example

如下图中的程序,handleReq中第10行,第14行,第16行中都有明显的问题。都有可能造成私有数据的泄露。本文的目的就是为了防止攻击者通过这些漏洞来大道敏感数据泄露的目的。

-w860

2.3 区分可信代码

如下图所示是作者设计的方案的工作流程,首先程序员标记出可信代码部分,标记为$\mathcal{T}$,$\mathcal{T}$中包含了合法解密私有数据的函数,或者是提供I/O的函数。其他的则标记为$\mathcal{U}$。一个比较好的策略是,让程序中的大部分逻辑都放在$\mathcal{U}$里。例如在上面的Web服务器例子里,$\mathcal{T}$可以由几部分组成:recv、send、read_file(network, I/O)、decrypt(加解密相关)、以及read_passwd(敏感数据的数据源)。上面Web服务器代码里的其余部分则都应该是$\mathcal{U}$。$\mathcal{T}$可以用普通编译器编译,而$\mathcal{U}$则需要用ConfLLVM来编译。

-w427

2.4 分割不可信代码部分的内存

作者提出的这个方案依赖于程序员去告诉ConfLLVm,私有数据是在哪里进入和离开$\mathcal{U}$。

程序员需要给的标记有两种: 1. 由于$\mathcal{U}$依赖于$\mathcal{T}$进行I/O通信,程序员需要在$\mathcal{T}$导出到$\mathcal{U}$的所有函数的函数签名上用类型修饰符private进行标注。 2. 需要在$\mathcal{U}$中的比较上层的定义中标记出private。例如全局变量,函数签名,结构体定义中。

第二种的标记是不被ConfLLVM信任的,如果与第一种出现了矛盾,可能在静态分析时就会出错,也可能在运行时出错。ConfLLVM会自动化地推断局部变量是否是私有数据。基于这些信息,ConfLLVM将$\mathcal{U}$的内存分为两部分,一部分是公开数据,一部分是私有数据。第三部分的内存则是$\mathcal{T}$使用。

如下图所示是前面提到的例子里,需要进行的标注如下

ConfLLVm会基于read_passwd函数的原型自动化地推断出passwd是一个私有数据,但是send的原型中,被发送数据不能是私有数据,因此在静态检查时就会报错。另外两个Bug就依赖于运行时检查来发现。

2.5 运行时检查

ConfLLVM插入运行时检查以确保: 1. 在运行时,指针属于他们被标记的类型的内存区域 2. $\mathcal{U}$不能读写 $\mathcal{T}$的内存区域 3. $\mathcal{U}$遵从Taint-aware CFI

$\mathcal{T}$中的代码是允许访问所有的内存的,然而$\mathcal{T}$的函数必须要检查他们的参数来确保$\mathcal{U}$传入的数据拥有正确的敏感性标签。举例来说,read_passwd函数需要检查[pass, pass+size-1]落在$\mathcal{U}$的私有内存区域内。

3. 内存分区方案

ConfLLVM通过程序员提供的标注,以及类型推断,静态地判断出每一个内存访问的内存,例如在$\mathcal{U}$中的每一个内存访问,都会判断一些目标地址是含有私有数据还是公开数据。

这里作者设计了两套内存分区以及运行时插桩方案,一种是基于MPX,一种是基于Intel Segment,如下图的编译后的代码可以很直观的感受。

4. Taint-aware CFI

作者设计了一个Taint-aware CFI方案用于确保攻击者无法通过修改$\mathcal{U}$的控制流来绕过运行时检查并泄露敏感数据。

典型的可以劫持程序控制流的攻击例如有修改返回地址,或者函数指针,间接跳转等。已有的方案结合Shadow Stacks和栈Canary来避免返回地址被修改,或者通过使用细粒度的污点追踪来确保函数指针不被修改。作者自己设计的Taint-aware CFI仅仅保证数据机密性。

作者设计的CFI方案: 1. 确保每一个间接跳转目标地址是合法的跳转地址,例如是某个函数入口,函数返回地址是合法的返回位置 2. 目标地址的污点状态与当前的寄存器污点状态一致。

通过使用一个magic-sequence比特序列来实现,挑选两个比特序列$M{Call}$和$M{Ret}$,都是59比特长,在$\mathcal{U}$的其他地方不会出现。每一个函数的入口都会有$M{Call}$,在$M{Call}$后面再跟着5个比特,表示(4个参数寄存器以及一个返回寄存器的的预期敏感性标签),每一个返回的位置,也都会有$M_{Ret}$,后面跟着1个比特的返回值寄存器的污点状态(为了64比特对齐,后面会补0)。

如下所示的示例代码:

插入$M{Call}$和$M{Ret}$后:

函数返回处的运行时检查:

5. 实现

作者在LLVM框架上实现了ConfLLVM,可以在Windows和Linux x64平台上使用(只在x64上实现是因为ConfLLVM依赖于x64的MPX或者段寄存器的支持)

作者修改Clang前端来添加了解析private这个类型修饰语,在生成的LLVM IR里插入了这些额外的metadata。ConfLLVM运行标准的LLVM IR优化,在优化都运行过了后,然后就会运行Type Qualifier Inference这个Pass。这个Pass用来传播类型修饰符给局部变。在类型修饰语推断之后,ConfLLVM就知道了每一个内存访问指令的内存的污点状态,再结合简单的数据流分析,编译器就可以静态地判断每个指令中的每一个寄存器的污点状态。

6. 评估

评估有3个指标: 1. 将ConfLLVM插桩的性能开销量化 2. 表明ConfLLVm在大程序上也是可以使用的 3. 检查方案是否能成功防御前面提到的攻击

6.1 测试性能

用SPEC CPU 2006 benchmarks来测量ConfLLVM的插桩开销,所有benchmarks的代码认为是不可信代码,用ConfLLVM编译,系统的native libc则认为是可信代码。Benchmark里没有私有数据,因此所有数据都是公有的,所以ConfLLVM编译出来的代码就是在确认所有的内存访问都是在公开内存里。如下所示是测试结果:

### 6.2 测试吞吐量

作者将OpenSSL认为是可信代码,NGINX的剩余部分则认为是不可信代码,并对代码进行了标注。目标是测试吞吐量,测试结果如下:

6.3 测试安全性

作者手动在3个程序里写了3个漏洞,然后用ConfLLVM进行编译,实验如下: 1. 在Mongoose Web Server里加入了一个缓冲区边界类型的漏洞,这个漏洞会将Private文件中的明文数据转移到栈上。攻击者可以利用漏洞,先请求这个Private文件,导致Private文件的明文被写到栈上,然后请求Public文件,这样能泄露出Private文件的数据。这种泄露方法在ConfLLVM编译下会失败,因为ConfLLVM将私有数据的栈和公有数据的栈分离了。 2. 修改了Minizip(一个文件压缩工具),可以显式地将被文件加密密码泄露到log文件里。ConfLLVM的Type Inference可以检测出这个泄露,只要我们将password标记为private。为了给ConfLLVM一些挑战,作者特意加了一些指针类型转换,使得ConfLLVM无法静态地判断出来这个错误,但是之后在运行时的检查里,仍然能够阻止泄露。 3. 写了个格式化字符串的漏洞,ConfLLVM能阻止格式化字符串漏洞进行泄露,只需要将printf作为不可信代码进行编译,这时候如果攻击者想通过格式化字符串进行泄露Private数据,就会被检查出来(通过Bound check)。