利用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中)。可以通过--recover-stack-vars参数调用mcsema-disass来使用它。有关示例,请参阅本文附带的代码,在“提升和多样化二进制文件”部分有更多描述。

全局变量

全局变量可以被程序中的所有函数访问。由于这些变量不绑定到特定函数,它们通常放在程序二进制文件的特殊部分(图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 Web服务器。

接下来,使用提供的脚本(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

我们可以看到example-lifted,即由McSema提升和重新创建的相同程序,行为相同:

 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博客上的文章。

免责声明

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

如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

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