安全缓解措施部署的挑战:ClangCFI与CFGuard的实战对比

本文详细分析了在osquery项目中部署Clang控制流完整性(ClangCFI)和Visual Studio控制流防护(CFGuard)的技术挑战,包括全程序分析要求、链接限制及现有代码库兼容性问题,揭示了安全缓解措施在实际部署中的开发成本与可用性权衡。

安全缓解措施部署的挑战

计划

原计划简单明确:为Windows版本的osquery启用CFGuard,为Linux版本启用ClangCFI。通过对比osquery测试套件中受保护与未受保护版本的差异进行量化评估。我们将贡献补丁回osquery代码库,最终形成一篇技术博客并增强osquery的安全性。

Windows版本的osquery启用CFGuard仅耗时约15分钟。此处是启用CFGuard的拉取请求,仅需修改一个CMake脚本中的两行代码。

然而,经过数周努力,我们仍未成功在Linux版本上启用ClangCFI。这种差异直接源于安全选择带来的深远影响。努力并非徒劳:我们报告了两个clang漏洞(),遇到了一个近期已解决的问题,并与clang开发者进行了深入交流。他们耐心解释了ClangCFI的细节,识别了我们遇到的问题,并提供了调试协助。

ClangCFI比CFGuard更严格

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

图1: 使用间接调用示例(ClangCFI、CFGuard)展示有效调用目标的差异。真正有效的目标以绿色高亮显示,其余为红色。

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

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

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

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

这些新要求是合理的:要求所有内容启用CFI确保程序没有未受保护的部分。LTO不仅允许全程序分析,还允许全程序优化,可能抵消CFI相关的性能损失。

CFGuard使用的较宽松验证标准安全性较低,但不需要全程序分析。启用CFGuard构建的对象验证间接调用;未启用CFGuard构建的对象则不验证。两种对象可以在同一程序中共存。然而,链接器必须感知CFGuard,以便在PE头中生成具有适当表和标志的二进制文件。

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

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

表1: 使用ClangCFI时的有效链接。这些链接在一般情况下是有效的,假设链接项之间存在间接调用。跨动态共享对象(DSO)的调用在实验性标志-f[no-]sanitize-cfi-cross-dso使用时有效。

Osquery按设计静态链接每个依赖项,包括libc++。这些依赖项又静态链接其他依赖项,依此类推。要为osquery启用ClangCFI,我们必须为整个osquery依赖树启用ClangCFI。正如下一节将看到的,这是一项艰巨的任务。尽管我们未来希望这样做,但无法为这篇博客证明这种时间投入是合理的。

CFGuard可以在每个编译单元级别应用。CFGuard的文档明确提到允许混合使用启用CFG和未启用CFG的对象和库[3]。跨DSO(即Windows术语中的DLL)的调用完全支持。这种灵活性对于为osquery启用CFGuard至关重要;我们为osquery本身启用了CFGuard,并链接到现有的未受保护依赖项。幸运的是,Windows提供了受CFGuard保护的系统库,当主程序映像支持CFGuard时会使用这些库。未受保护的代码仅限于构建osquery时使用的静态库。

ClangCFI对某些代码库过于严格

ClangCFI对某些代码库过于严格。这不是clang的错:有些代码使用了可能不完全符合标准的快捷方式和便利方法。我们在尝试为strongSwan启用ClangCFI时遇到了这个问题。我们的目标是尝试一个比osquery更小的示例,并为我们的VPN解决方案Algo创建一个安全增强版本的strongSwan。

图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 设计