控制流验证工具设计文档

目标

本文档概述了一个外部工具,用于验证 Clang 的控制流完整性 (CFI) 方案(-fsanitize=cfi)所实现的保护机制。该工具在给定二进制文件或 DSO 的情况下,应推断间接控制流操作是否受到 CFI 保护,并应以人类可读的形式输出这些结果。

该工具还应作为 Clang 持续集成测试框架的一部分添加,以便对编译器的修改确保 CFI 保护方案仍然存在于最终的二进制文件中。

位置

该工具将作为 LLVM 工具链的一部分存在,并将位于相对于 LLVM 主干的 “/llvm/tools/llvm-cfi-verify” 目录中。它将通过两种方法进行测试

  • 单元测试,用于验证代码段,位于 “/llvm/unittests/tools/llvm-cfi-verify” 中。

  • 集成测试,位于 “/llvm/tools/clang/test/LLVMCFIVerify” 中。这些集成测试是 clang 作为持续集成框架的一部分,确保识别对编译器的更新,这些更新会降低间接控制流指令的 CFI 覆盖率。

背景

该工具将通过分析输出的机器代码,持续验证 CFI 指令是否在所有间接控制流周围得到正确实现。机器代码的分析非常重要,因为它确保链接器或编译器中存在的任何错误都不会破坏最终交付二进制文件中的 CFI 保护。

未受保护的间接控制流指令将被标记以进行人工审查。这些意外的控制流可能仅仅是在 CFI 的编译器实现中没有考虑到的情况(例如,为了方便 switch 语句而进行的间接跳转可能没有得到充分保护)。

未来可能会扩展此工具,以标记不必要的 CFI 指令(例如,围绕对非多态基类型的静态调用的 CFI 指令)。这种类型的指令没有安全隐患,但可能会带来性能影响。

设计思路

该工具将从二进制文件和 DSO 的机器代码格式中反汇编,并分析反汇编的机器代码。该工具将检查虚调用和间接函数调用。该工具还将检查间接跳转,因为内联函数和跳转表也应受到 CFI 保护。非虚调用(-fsanitize=cfi-nvcall)和类型转换检查(-fsanitize=cfi-*cast*)由于字节码提供的信息不足而未实现。

该工具的工作方式是在反汇编代码中搜索间接控制流指令。将从围绕“目标”控制流指令的一小段指令缓冲区生成控制流图。如果目标指令是被分支到的,则分支的 fallthrough 应该是 CFI 陷阱(在 x86 上,这是一个 ud2 指令)。如果目标指令是条件跳转的 fallthrough(即紧随其后),则条件跳转目标应该是 CFI 陷阱。如果间接控制流指令不符合这些格式之一,则目标将被标记为 CFI 未受保护。

请注意,在上面概述的第二种情况(其中目标指令是条件跳转的 fallthrough)中,如果目标表示接受参数的虚调用,则这些参数可能会在分支之后但在目标指令之前被压入堆栈。在这些情况下,会构建一个辅助的“溢出图”,以确保间接跳转/调用使用的寄存器参数在临时期间的任何时候都不会从堆栈中溢出。如果没有影响目标寄存器的溢出,则目标被标记为 CFI 受保护。

其他设计说明

只有标记为可执行的机器代码段才会受到此分析。不可执行的段不需要分析,因为这些段中存在的任何执行都已经违反了控制流完整性。

稍后可能会进行适当的扩展,以包括跨 DSO 边界的间接控制流操作的分析。目前,这些 CFI 功能仅处于实验阶段,ABI 不稳定,因此不适合进行分析。

该工具目前仅支持 x86、x86_64 和 AArch64 架构。