控制流验证工具设计文档

目标

本文档概述了一个外部工具,用于验证 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*)。

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

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

其他设计说明

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

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

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