利用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: 简单函数的栈变量在源代码级别和二进制级别都显示。在二进制级别,没有单个变量的概念,只有一大块内存中的字节。
当攻击者将错误转化为漏洞利用时,他们通常依赖栈变量处于特定顺序。多编译器可以通过生成程序变体来缓解这类漏洞利用,其中没有两个变体的栈变量顺序相同。我们希望在二进制文件中启用这种栈变量洗牌,但有一个问题:在机器代码级别没有栈变量的概念(图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)。与栈变量一样,全局变量的特定顺序可以被攻击者利用。
|
|
图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。
四种不同情况可以通过以下程序调用来演示:
|
|
我们可以看到example-lifted,即由McSema提升和重新创建的相同程序,行为相同:
|
|
现在,让我们多样化提升的示例程序。首先,安装多编译器。接下来,编辑lift.sh脚本以指定多编译器安装的路径。
是时候构建多样化版本了。使用diversify参数运行脚本(./lift.sh diversify)以生成多样化二进制文件。多样化的示例在二进制级别看起来与原始版本不同(图3),但具有相同的功能:
|
|
图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
页面内容 近期文章 我们构建了MCP一直需要的安全层 在废弃硬件中利用零日漏洞 Inside EthCC[8]:成为智能合约审计员 使用Vendetect大规模检测代码复制 构建安全消息传递很难:对Bitchat安全辩论的细致看法 © 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。