利用DARPA的CFAR技术保护软件免受攻击

本文介绍了DARPA的"网络容错攻击恢复"(CFAR)项目如何通过并行运行多个软件变体来检测异常行为,从而保护二进制软件免受零日漏洞和内存破坏攻击。文章详细讲解了McSema工具在恢复堆栈变量和全局变量语义方面的技术实现。

利用DARPA的CFAR技术保护软件免受攻击

今天我们要讨论的是DARPA"网络容错攻击恢复"(CFAR)项目中的一个难题:如何自动保护软件免受零日漏洞利用、内存破坏以及许多尚未发现的漏洞影响。你可能会想:“为什么要这么麻烦?难道不能直接用栈保护、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"源自"machine code semantics”)。然而,为了准确转换CFAR所需的变体,McSema还必须恢复程序语义。

我们正在积极努力恢复越来越多的程序语义,许多常见用例已经得到支持。在下一节中,我们将讨论如何处理两个特别重要的语义:栈变量和全局变量。

栈变量

编译器可以将支持函数变量的数据放在几个位置之一。程序变量最常见的位置是栈,这是一个专门用于存储临时信息且易于被调用函数访问的内存区域。编译器存储在栈上的变量被称为…栈变量!

1
2
3
4
5
int sum_of_squares(int a, int b) {
  int a2 = a * a;
  int b2 = b * b;
  return a2+b2;
}

图1:简单函数的栈变量在源代码级别和二进制级别都显示出来。在二进制级别,没有单个变量的概念,只有大块内存中的字节。

当攻击者将漏洞转化为利用时,他们经常依赖于栈变量处于特定顺序。多编译器可以通过生成程序变体来缓解这类攻击,其中没有两个变体的栈变量顺序相同。我们希望在二进制文件中启用这种栈变量洗牌功能,但存在一个问题:在机器代码级别没有栈变量的概念(图1)。相反,栈只是一个大的连续内存块。McSema忠实地模拟这种行为,并将程序栈视为不可分割的整体。这当然使得洗牌栈变量变得不可能。

栈变量恢复

将表示栈的内存块转换为单个变量的过程称为栈变量恢复。McSema将栈变量恢复实现为一个三步过程。

首先,McSema在反汇编期间通过反汇编程序(如IDA Pro)的启发式方法和DWARF调试信息(如果有的话)识别栈变量边界。之前有研究在没有这些提示的情况下识别栈变量边界,我们计划在未来利用这些研究。其次,McSema尝试识别程序中的哪些指令引用了哪些栈变量。必须准确识别每个引用,否则生成的程序将无法运行。最后,McSema为每个恢复的栈变量创建一个LLVM级变量,并重写指令以引用这些LLVM级变量而不是之前的整体栈块。

栈变量恢复对许多函数有效,但并不完美。当McSema遇到具有以下特征的函数时,它将默认采用将栈视为整体的经典行为:

  • 可变参数函数。使用可变数量参数(如常见的printf系列函数)的函数具有可变大小的栈帧。这种变化使得难以确定哪些指令引用了哪些栈变量。
  • 间接栈引用。编译器也依赖于栈变量的预定布局,并会生成通过不相关变量的地址访问变量的代码。
  • 无栈帧指针。作为优化,栈帧指针可以用作通用寄存器。这种优化使我们难以检测可能的间接栈引用。

栈变量恢复是CFG恢复过程的一部分,目前已在IDAPython CFG恢复代码中实现(在collect_variable.py中)。可以通过mcsema-disass的–recover-stack-vars参数调用它。有关示例,请参阅本文附带的代码,这在"提升和多样化二进制文件"部分有更多描述。

全局变量

全局变量可以被程序中的所有函数访问。由于这些变量不绑定到特定函数,它们通常被放置在程序二进制文件的一个特殊部分(图2)。与栈变量一样,全局变量的特定顺序可能被攻击者利用。

1
2
3
4
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恢复过程将进一步用真实的控制流图填充文件,引用预先填充的全局变量。我们稍后将展示使用全局变量恢复的示例。

提升和多样化二进制文件

在这篇博文的剩余部分,我们将通过多编译器生成新程序变体的过程称为"多样化"。对于这个具体示例,我们将提升和多样化一个使用异常处理(包括catch-all子句)和全局变量的简单C++应用程序。虽然这只是一个简单示例,但程序语义恢复旨在处理大型实际应用程序:我们的标准测试程序是Apache2网络服务器。

接下来,使用提供的脚本(lift.sh)构建并提升程序。需要编辑脚本以匹配你的McSema安装。

运行lift.sh后,你应该有两个程序:example和example-lift,以及一些中间文件。

example程序对两个数字进行平方运算,并将结果传递给set_admin函数。如果两个数字都是5,则程序抛出std::runtime_error异常。如果数字是0,则全局变量is_admin设置为true。最后,如果没有向程序提供两个数字,则抛出std::out_of_range。

可以通过以下程序调用演示四种不同情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ./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行为相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ./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),但功能相同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ ./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:正常提升的二进制文件(左)及其多样化等效文件(右)。两个二进制文件功能相同,但在二进制级别看起来不同。二进制多样化通过防止某些类别的漏洞转化为利用来保护软件。

在你最喜欢的反汇编程序中打开example-lifted和example-diversified。你的二进制文件可能与截图中的不完全相同,但它们应该彼此不同。

让我们回顾一下我们所做的事情。这真的很神奇。我们首先构建了一个使用异常和全局变量的简单C++程序。然后我们将程序转换为LLVM位码,识别栈和全局变量,并保留基于异常的控制流。然后我们使用多编译器对其进行转换,并创建了一个与原始程序功能相同的新多样化二进制文件。

虽然这只是一个小例子,但这种方法可以扩展到更大的应用程序,并提供了一种快速创建多样化程序的方法,无论是从源代码还是从先前的程序二进制文件开始。

结论

我们首先要感谢DARPA,没有他们这项工作就不可能完成,他们为CFAR和其他伟大的研究项目提供了持续的资金支持。我们还要感谢我们的队友——Galois、Immunant和UCI——感谢他们的辛勤工作,创建了多编译器、转换方法,为变体提供了等价性保证,并使一切协同工作。

我们正在积极改进McSema中的栈和全局变量恢复。这些更高级别的语义不仅会创造更多的多样化和转换机会,还会允许更小、更精简的位码,更快的重新编译二进制文件,以及更彻底的分析。

我们相信CFAR和类似技术有着光明的未来:每台机器的可用核心数量持续增加,对安全计算的需求也在增加。许多软件包无法利用这些核心来提高性能,因此很自然地利用这些空闲核心来增强安全性。McSema、多编译器和其他CFAR技术展示了我们如何将这些额外的核心用于更强的安全保证。

如果你认为其中一些技术可以应用于你的软件,请联系我们。我们很乐意听取你的意见。要了解更多关于CFAR、多编译器以及在该计划下开发的其他技术,请阅读我们的队友在Galois博客和Immunant博客上的文章。

免责声明

所表达的观点、意见和/或发现属于作者,不应被解释为代表国防部或美国政府的官方观点或政策。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计