深入解析ClangCFI与CFGuard:安全缓解措施部署的挑战与权衡

本文详细对比了ClangCFI和CFGuard两种控制流完整性技术的实现差异,探讨了在真实代码库中部署安全缓解措施时面临的技术挑战、兼容性问题和开发成本,为安全工程师提供了实践指导。

部署安全缓解措施的挑战 - Trail of Bits博客

计划

计划很简单:我们将在Windows版本的osquery上启用CFGuard,在Linux版本的osquery上启用ClangCFI。通过osquery测试套件中受保护和未受保护构建之间的差异来进行量化测量。我们将把补丁贡献给osquery代码库,最终形成一篇优秀的博客文章和一个更安全的osquery。

我们在大约15分钟内就让Windows版本的osquery在CFGuard下运行。这是为Windows版osquery启用CFGuard的拉取请求。更改仅涉及一个CMake脚本中的两行代码。

然而,经过数周的努力,我们仍然无法在Linux构建上启用ClangCFI。这种差异直接源于善意的安全选择所带来的深远影响。这些努力并非徒劳;我们报告了两个clang错误(一个两个),遇到了一个最近已解决的问题,并与clang开发人员进行了非常有见地的交流。

ClangCFI比CFGuard更严格

对于每个受保护的间接调用,ClangCFI允许的有效目标比CFGuard少。这是好事:更少的目标意味着将漏洞转化为攻击的途径更少。ClangCFI也比CFGuard检测到更多潜在错误(例如强制转换检查、确保虚函数调用目标位于对象层次结构中等)。

图1:使用icall示例的间接调用有效目标差异(ClangCFI、CFGuard) 真正的有效目标以绿色突出显示,其他所有内容均为红色

每个CFI方案允许的具体内容对可用性具有关键影响。对于ClangCFI,间接调用目标必须与调用站点的类型签名匹配。ClangCFI的虚方法调用检查甚至更严格。例如,ClangCFI检查目标方法是否属于同一对象层次结构。对于CFGuard,间接调用目标可以是有效的函数入口点[1]。

图2:ClangCFI、CFGuard有效间接调用目标的理想化视图及其与(理想化)有效间接调用目标集的比较

ClangCFI的类型签名验证和虚方法调用检查需要全程序分析。全程序分析需求导致两个额外要求:

  1. 通常,组成最终程序的每个链接对象和静态库都必须使用CFI构建[2]
  2. 使用ClangCFI时需要链接时优化(LTO),因为全程序分析直到链接时才能进行

ClangCFI是全有或全无,CFGuard是增量式的

通常,ClangCFI必须为程序中的每个目标文件和静态库启用:将启用CFI的代码与非CFI代码链接是错误的[2]。这个错误很容易犯但难以识别,因为链接器不会检查对象的ClangCFI保护。链接器不会报告错误,但生成的可执行文件将无法通过运行时CFI检查。

表1:使用ClangCFI时的有效链接

链接类型 是否有效
CFI对象 + CFI对象
CFI对象 + 非CFI对象
跨DSO调用 实验性支持

Osquery按设计静态链接每个依赖项,包括libc++。这些依赖项又静态链接其他依赖项,依此类推。要为osquery启用ClangCFI,我们必须为整个osquery依赖树启用ClangCFI。正如我们将在下一节中看到的,这是一项艰巨的任务。

CFGuard可以在每个编译单元级别应用。CFGuard的文档明确提到允许混合使用启用CFG和未启用CFG的对象和库[3]。完全支持跨DSO(即Windows术语中的DLL)调用。这种灵活性对于为osquery启用CFGuard至关重要;我们为osquery本身启用了CFGuard,并与现有的未受保护依赖项链接。

ClangCFI对某些代码库过于严格

ClangCFI对某些代码库过于严格。这不是clang的错:有些代码使用的快捷方式和便利性可能不完全符合标准。我们在尝试为strongSwan启用ClangCFI时遇到了这个问题。

图3:现有真实代码与ClangCFI和CFGuard间接调用目标的关系 存在有效、程序员预期的目标落在ClangCFI和CFGuard定义的域之外

我们无法创建启用CFI的strongSwan版本,因为strongSwan的核心组件libstrongswan对C使用类似OOP的系统。该系统使用无法通过ClangCFI严格检查的接口包装大多数间接调用。ClangCFI在技术上是正确的:调用者和被调用者的类型签名应该匹配。但实际上,存在类型签名不匹配的已发布代码。

幸运的是,ClangCFI有一个放宽严格性的功能:CFI黑名单。黑名单将为匹配正则表达式的源文件、函数或类型禁用CFI检查。不幸的是,在这种情况下,几乎每个间接调用站点都必须被列入黑名单,使得CFI实际上无用。

CFGuard不太可能引起相同的问题:(可能)存在间接调用到函数中间的代码,但这种代码比类型签名不匹配的情况罕见得多。

结论

从安全角度来看,ClangCFI比CFGuard"更好"。它更严格,要求保护整个程序,并测试更多运行时错误。可以利用ClangCFI来保护大型复杂代码库:优秀的Google Chrome团队就是这样做的。然而,增强的安全性带来了高昂的代价。启用ClangCFI可能变成一项复杂的任务,需要大量的开发人员时间和严格的测试。

相反,CFGuard要灵活得多。程序可以混合使用受保护和未受保护的代码,CFGuard破坏现有代码的可能性要小得多。这些妥协使得CFGuard更容易为现有代码库启用。

我们使用ClangCFI和CFGuard的经验反映了这些权衡。启用ClangCFI的osquery比启用CFGuard的osquery更安全。然而,启用CFGuard的Windows版osquery现在已经存在。经过数周的试错,启用ClangCFI的Linux版osquery仍在进行中。


[1] 这并不完全正确。例如,被抑制的函数是函数入口点,但是无效的间接调用目标。

[2] 同样,这并不完全正确;混合规则有特定的例外。例如,CFI支持库不是用CFI构建的。如果非CFI对象中的每个函数都只被直接调用,链接CFI和非CFI对象是可以的。参见Evgeniy Stepanov的此评论

[3] 来自此页面:"…启用CFG和未启用CFG的代码混合执行良好。"

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