导语:在本文的部分篇幅中,我们将展示McSema一些鲜为人知的特征,并解释开发缘由,也许最让大家感兴趣的是,如何使用McSema和UCI多编译器来强化现成的二进制文件以防止被利用。
今天,我们(Trail of Bits)将要讨论我们正在努力解决的一个问题,这个问题是DARPA网络容错攻击恢复(CFAR)计划的一个组成部分:自动保护软件免受零时差攻击、内存损坏还有许多当前未知的bug。你可能在想为什么要这么麻烦,难道不能只使用堆栈保护,CFG或CFI等漏洞利用缓解来编译代码吗?当然,这些缓解措施是很棒的,但需要源代码和对源代码修改后才能构建过程。在许多情况下,更改构建过程或更改程序源代码是不切实际的。这就是为什么我们的CFAR解决方案可以保护源在不可用/不可编辑状态下的二进制安装。
CFAR看似非常直观简单。系统并行运行软件的多个版本或“变体”,并通过比较这些变体来识别某些个版本在行为上何时与其他版本不同。这个想法类似于入侵检测系统,该系统将程序行为与在相同输入结果上运行的自身变体进行比较,而不是与过去行为的模型进行比较。当系统检测到行为差异时,它可以推断出发生了异常且可能是恶意的事情。
像所有DARPA计划一样,CFAR是一个庞大而棘手的研究问题。我们只处理它的一小部分。我们和我们的队友——Galois,Immunant和UCI共同创作了这篇文章,他们每个人都对CFAR项目贡献了很多细节。
我们非常乐意谈论CFAR,不仅因为它是一个棘手的相关问题,也是由于我们的一个工具——McSema,我们团队基于LLVM的多功能解决方案的一份子。在本文的部分篇幅中,我们将展示McSema一些鲜为人知的特征,并解释开发缘由,也许最让大家感兴趣的是,如何使用McSema和UCI多编译器来强化现成的二进制文件以防止被利用。
我们的CFAR团队
CFAR的总体目标是在不影响核心功能的情况下检测现有软件中的故障并从中恢复。我们团队的职责是生成一组最佳变体,以检测和减少引发故障的输入。其他团队负责专门的执行环境、红队,等等。Galois在其关于CFAR的博客文章有更详细的描述。
这些变体的行为必须与原始版本完全相同,并能证明对于所有有效输入都能保持不变。我们的队友已经开发了转换,并为具有可用源代码的程序提供了等效保证。团队使用Clang / LLVM工具链设计了一个基于多编译器的变体生成解决方案。
McSema的角色
考虑到源代码可能不适用于专有或较旧的应用程序,我们一直致力于生成二进制软件的程序变体。我们团队的基于源代码的工具链在LLVM中间表示(IR)级别工作。IR级别的转换和加固程序允许我们在不改变程序源代码的情况下操作程序结构。使用McSema,我们可以将二进制程序转换为LLVM IR,并能重新利用相同的组件让源级别的二进制变体生成。
为了能够准确的翻译CFAR程序,我们需要弥合机器级语义和程序级语义之间的差距。机器级语义是由单个指令引起的处理器和内存状态的更改。程序级语义(例如,函数,变量,异常和try / catch块)是表示程序行为的更抽象的概念。McSema被设计成机器级语义的翻译器(名称“McSema”源自“机器代码语义”)。但是,要准确转换CFAR所需的变体,McSema也必须恢复程序语义。
我们正在积极努力恢复越来越多的程序语义,并且已经支持许多常见的用例。在下一节中,我们将讨论如何处理两个特别重要的语义:堆栈变量和全局变量。
堆栈变量
编译器可以将数据支持函数变量放在任意几个位置中。程序变量里最常见的位置是堆栈,这是一个专门用于存储临时信息并且易于被调用函数访问的内存区域。编译器存储在堆栈中的变量称为堆栈变量。
int sum_of_squares(int a, int b) { int a2 = a * a; int b2 = b * b; return a2+b2; }
图1:在源代码级别和二进制级别显示的简单函数的堆栈变量。在二进制级别,没有单个变量的概念,只有大块内存中的字节。
当攻击者将bug转化为漏洞时,他们通常依赖于特定顺序的堆栈变量。多编译器可以通过生成程序变体来减少此类漏洞,其中没有哪两个变体会具有相同顺序的堆栈变量。我们希望为二进制文件启用堆栈变量重排,但是存在一个问题:在机器代码级别没有堆栈变量的概念(图1)。相反,堆栈只是一个大的连续内存块。McSema如实的模拟了这种行为,并将程序堆栈视为不可分割的一块。当然,这会使得堆栈变量无法进行重排。
堆栈可变恢复
将表示堆栈的内存块转换为单个变量的过程称为堆栈变量恢复。McSema将堆栈变量恢复分为三个步骤实现。
首先,McSema通过反汇编程序(例如IDA Pro)的启发式算法以及基于DWARF(如果存在的话)的调试信息,在反汇编期间识别堆栈变量边界。以前的研究是在没有这些提示的情况下识别堆栈变量边界,但我们计划在将来使用这些提示。其次,McSema尝试识别程序中,哪些指令在引用哪个堆栈变量。必须准确识别每个参考,否则生成的程序将无法运行。最后,McSema为每个恢复的堆栈变量创建一个LLVM级变量,并重写指令以引用这些LLVM级变量,而不是先前的单片堆栈块。
堆栈变量恢复适用于许多功能,但它并不完美。当遇到具有以下特征的函数时,McSema将默认采用将堆栈视为整体块的经典行为:
· Varargs功能。使用可变数量参数的函数(如常见的printf函数系列),具有可变大小的堆栈帧。这种差异会导致很难确定哪个指令引用哪个堆栈变量。
· 间接堆栈引用。编译器还依赖于堆栈变量的预定布局,并将生成通过不相关变量的地址访问变量的代码。
· 没有堆栈帧指针。作为优化,堆栈帧指针可以用作通用寄存器。这种优化使我们很难检测到可能的间接堆栈引用。
堆栈变量恢复是CFG恢复过程的一部分,目前在IDAPython CFG恢复代码(在collect_variable.py中)中实现。它可以通过–recover-stack-vars参数调用mcsema-disass。有关示例,请参阅此篇文章随附的代码“升级和多样化二进制”部分对此进行了详细介绍。
全局变量
程序中的所有函数都可以访问全局变量。由于这些变量与特定函数无关,因此通常将它们放在程序二进制文件的特殊部分中(图2)。与堆栈变量一样,攻击者可以利用全局变量的特定顺序。
bool is_admin = false; int set_admin(int uid) { is_admin = 0 == uid; }
图2:从源代码级别和机器代码级别看到的全局变量。全局变量通常放在程序的特殊部分(在本例中为.bss)
与堆栈一样,McSema将每个数据部分视为一大块内存。堆栈和全局变量之间的一个主要区别是McSema知道全局变量的起始位置,因为它们直接从多个位置引用。不过,这并不足以使全局变量布局混乱。McSema还需要知道每个变量的结束位置,这更难。目前,我们依靠DWARF调试信息来识别全局变量大小,但期待实现可用于没有DWARF信息的二进制文件的方法。
目前,全局变量恢复与正常的CFG恢复(在var_recovery.py中)分开实现。该脚本创建一个“空”CFG,仅填充全局变量定义。正常的CFG恢复过程将使用实际控制流图进一步填充文件,引用预先填充的全局变量。我们稍后将展示使用全局变量恢复的示例。
提升和多样化二进制
在本文的其余部分,我们将通过多编译器引用生成新程序变体的过程称为“多样化”。对于此特定示例,我们将提升和多样化使用异常处理的简单C ++应用程序(包括捕获-all子句)和全局变量。虽然这只是一个简单的例子,程序语义恢复意味着可以处理大型实际应用程序:我们的标准测试程序是Apache2 Web服务器。
首先,让我们熟悉标准的McSema工作流程(即没有任何多样化),即将示例二进制文件提升到LLVM IR,然后将该IR编译回可运行的程序。要开始使用,请构建并安装McSema。我们在官方McSema README中提供详细说明。
接下来,使用提供的脚本(lift.sh)构建并提升程序。需要编辑脚本以匹配您的McSema安装。
运行lift.sh后,你应该有两个程序:example和example-lift,以及一些中间文件。
示例程序将两个数字对齐并将结果传递给set_admin函数。如果两个数字都是5,那么程序将抛出std :: runtime_error异常。如果数字为0,则全局变量is_admin设置为true。最后,如果没有向程序提供两个数字,那么它会抛出std :: out_of_range。
可以通过以下程序调用来演示四种不同的情况:
$ ./example Starting example program Index out of range: Supply two arguments, please $ ./example 0 0 Starting example program You are now admin. $ ./example 1 2 Starting example program You are not admin. $ ./example 5 5 Starting example program Runtime error: Lucky number 5
我们可以看到,示例的提升程序,与McSema提升并重新创建的程序完全相同:
$ ./example-lifted Starting example program Index out of range: Supply two arguments, please $ ./example-lifted 0 0 Starting example program You are now admin. $ ./example-lifted 1 2 Starting example program You are not admin. $ ./example-lifted 5 5 Starting example program Runtime error: Lucky number 5
现在,让我们对提升的示例程序进行多样化。首先,安装多编译器。接着,编辑lift.sh脚本以指定多编译器安装的路径。
现在是构建多样化版本的时候了。使用diversify参数(./lift.sh diversify)运行脚本以生成多样化的二进制文件。 多样化的示例在二进制级别上看起来与原始级别不同(图3),但具有相同的功能:
$ ./example-diverse Starting example program Index out of range: Supply two arguments, please $ ./example-diverse 0 0 Starting example program You are now admin. $ ./example-diverse 1 2 Starting example program You are not admin. $ ./example-diverse 5 5 Starting example program Runtime error: Lucky number 5
图3:正常提升二进制(左)及其多样化等价物(右)。两个二进制文件在功能上是相同的,但在二进制级别上看起来不同。二进制多样化通过防止某些类型的bug变成漏洞来保护软件
在您最喜爱的反汇编程序中打开示例- 提升和示例- 多样化。您的二进制文件可能与屏幕截图中的二进制文件不同,但它们应该彼此不同。
让我们回顾一下我们做了什么。这真的很神奇。我们首先构建一个使用异常和全局变量的简单C ++程序。然后我们将程序翻译成LLVM bitcode,识别堆栈和全局变量,并保留基于异常的控制流程。然后我们使用多编译器对其进行了转换,并创建了一个新的,多样化的二进制文件,其功能与原始程序相同。
虽然这只是一个小例子,但这种方法可以扩展到更大的应用程序,并且提供了一种快速创建多样化程序的方法,无论是从源代码还是以前的程序二进制文件开始。
结论
我们首先要感谢DARPA,为CFAR和其他伟大的研究项目提供持续的资金,没有他们这项工作是不可能的完成的。我们还要感谢我们的队友–Galois,Immunant和UCI,为创建多编译器,转换,为变体提供等效保证以及使所有内容协同工作所做的辛勤工作。
我们正积极致力于改善McSema的堆栈和全局变量恢复。这些更高级别的语义不仅可以创造更多样化和转换机会,而且还可以实现更小、更精简的bitcode,更快的重新编译二进制文件以及更彻底的分析。
我们相信CFAR和类似技术有着光明的未来:每台机器的可用内核数量不断增加,安全计算需求也在不断增加。许多软件包无法利用这些内核来提高性能,因此很自然的将备用内核用于保障安全性。McSema,多编译器和其他CFAR技术展示了我们如何将这些额外的内核用于更强大的安全保障。
如果您认为其中一些技术可以应用于您的软件,请与我们联系。我们很乐意听取您的意见。要了解有关CFAR,多编译器以及在此计划下开发的其他技术的更多信息,请阅读Galois博客和Immunant博客上的队友博客文章。
免责声明
本文所表达的观点、意见和调查结果均为作者的观点,不作为国防部或美国政府的官方观点或政策。