截至 2016 年,Android 上约 86% 的漏洞与内存安全相关。大多数漏洞都是攻击者通过更改应用的正常控制流来利用的,目的是利用被攻击应用的全部特权执行任意恶意活动。控制流完整性 (CFI) 是一种安全机制,可阻止更改已编译二进制文件的原始控制流图,从而大大增加执行此类攻击的难度。
在 Android 8.1 中,我们在媒体堆栈中启用了 LLVM 的 CFI 实现。在 Android 9 中,我们在更多组件以及内核中启用了 CFI。系统 CFI 默认处于启用状态,但您需要启用内核 CFI。
LLVM 的 CFI 需要使用链接时优化 (LTO)进行编译。LTO 会保留对象文件的 LLVM 位代码表示形式,直到链接时,这让编译器可以更好地推断可以执行哪些优化。启用 LTO 可以减小最终二进制文件的大小并提高性能,但会增加编译时间。在 Android 上的测试中,LTO 和 CFI 的组合对代码大小和性能的影响可忽略不计;在少数情况下,两者都有所改进。
如需详细了解 CFI 以及如何处理其他前向控制检查,请参阅LLVM 设计文档。
示例和源代码
CFI 由编译器提供,并在编译期间将检测代码添加到二进制文件中。我们在 Clang 工具链和 AOSP 中的 Android 构建系统中支持 CFI。
对于 /platform/build/target/product/cfi-common.mk
中的组件集,Arm64 设备默认启用 CFI。它还在一组媒体组件的 makefile/blueprint 文件中直接启用,例如 /platform/frameworks/av/media/libmedia/Android.bp
和 /platform/frameworks/av/cmds/stagefright/Android.mk
。
实施系统 CFI
如果您使用 Clang 和 Android 构建系统,则默认启用 CFI。由于 CFI 有助于确保 Android 用户的安全,因此您不应将其停用。
实际上,我们强烈建议您为其他组件启用 CFI。理想的候选项是特权原生代码或处理不受信任的用户输入的原生代码。如果您使用的是 clang 和 Android 构建系统,则可以通过向 makefile 或 blueprint 文件添加几行代码来在新组件中启用 CFI。
在 makefile 中支持 CFI
要在 makefile(例如 /platform/frameworks/av/cmds/stagefright/Android.mk
)中启用 CFI,请添加
LOCAL_SANITIZE := cfi # Optional features LOCAL_SANITIZE_DIAG := cfi LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
LOCAL_SANITIZE
指定 CFI 作为构建期间的清理器。LOCAL_SANITIZE_DIAG
开启 CFI 的诊断模式。诊断模式会在崩溃期间在 logcat 中输出额外的调试信息,这在开发和测试您的构建时非常有用。但是,请务必在生产版本中移除诊断模式。LOCAL_SANITIZE_BLACKLIST
允许组件有选择地为各个函数或源文件停用 CFI 检测代码。您可以将黑名单作为最后的手段来修复可能存在的任何面向用户的问题。如需了解更多详情,请参阅停用 CFI。
在 blueprint 文件中支持 CFI
要在 blueprint 文件(例如 /platform/frameworks/av/media/libmedia/Android.bp
)中启用 CFI,请添加
sanitize: { cfi: true, diag: { cfi: true, }, blacklist: "cfi_blacklist.txt", },
问题排查
如果您要在新组件中启用 CFI,您可能会遇到一些函数类型不匹配错误和汇编代码类型不匹配错误问题。
发生函数类型不匹配错误的原因是,CFI 限制间接调用仅跳转到与调用中使用的静态类型具有相同动态类型的函数。CFI 限制虚拟和非虚拟成员函数调用仅跳转到作为用于进行调用的对象的静态类型的派生类的对象。这意味着,当您的代码违反其中任何一个假设时,CFI 添加的检测代码将中止。例如,堆栈轨迹显示 SIGABRT,并且 logcat 包含一行关于控制流完整性发现不匹配的信息。
要解决此问题,请确保被调用的函数具有与静态声明的类型相同的类型。以下是两个示例 CL
另一个可能的问题是尝试在包含对汇编的间接调用的代码中启用 CFI。由于汇编代码未类型化,因此这会导致类型不匹配。
要解决此问题,请为每个汇编调用创建原生代码封装容器,并为这些封装容器提供与调用指针相同的函数签名。然后,封装容器可以直接调用汇编代码。由于直接分支不受 CFI 检测代码的检测(它们无法在运行时重新指向,因此不会造成安全风险),因此这将解决此问题。
如果汇编函数过多且无法全部修复,您还可以将包含对汇编的间接调用的所有函数列入黑名单。不建议这样做,因为它会停用对这些函数的 CFI 检查,从而扩大攻击面。
停用 CFI
我们没有观察到任何性能开销,因此您无需停用 CFI。但是,如果存在面向用户的影响,您可以通过在编译时提供清理器黑名单文件来有选择地为各个函数或源文件停用 CFI。黑名单指示编译器停用指定位置的 CFI 检测代码。
Android 构建系统为每个组件的黑名单提供支持(允许您选择将不接收 CFI 检测代码的源文件或各个函数),适用于 Make 和 Soong。如需详细了解黑名单文件的格式,请参阅上游 Clang 文档。
验证
目前,没有专门针对 CFI 的 CTS 测试。相反,请确保在启用或未启用 CFI 的情况下,CTS 测试均通过,以验证 CFI 是否对设备造成影响。