- 原文地址: Control Flow Integrity in the Android kernel
- 原文作者: Android Developers Blog
- 译文出自: 掘金翻译计划
- 本文永久链接: github.com/xitu/gold-m…
- 译者: nanjingboy
- 校对者: gs666
由 Android 安全研究工程师 Sami Tolvanen 发布
Android 的安全模型由 Linux 内核强制执行,这将诱使攻击者将其视为攻击目标。我们在已发布的 Android 版本和 Android 9 上为 加强内核 投入了大量精力,我们将继续这项工作,通过将关注点放在 基于编译器的安全缓解措施 上以防止代码重用攻击。
Google 的 Pixel 3 将是第一款在内核中实施 LLVM 前端 控制流完整性(CFI) 的设备,我们已经实现了 Android 内核版本 4.9 和 4.14 中对 CFI 的支持 。这篇文章描述了内核 CFI 的工作原理,并为开发人员在启用该功能时可能遇到的常见问题提供了解决方案。
防止代码重用攻击
利用内核的常用方法是使用错误来覆盖存储在内存中的函数指针,例如存储了回调函数的指针,或已被推送到堆栈的返回地址。这允许攻击者执行任意内核代码来完成利用,即使他们不能注入自己的可执行代码。这种获取代码执行能力的方法在内核中特别受欢迎,因为它使用了大量的函数指针,以及使代码注入更具挑战性的现有内存保护机制。
CFI 尝试通过添加额外的检查来确认内核控制流停留在预先设计的版图中,以便缓解这类攻击。尽管这无法阻止攻击者利用一个已存在的 bug 获取写入权限,从而更改函数指针,但它会严格限制可被其有效调用的目标,这使得攻击者在实践中利用漏洞的过程变得更加困难。
图 1. 在 Android 设备内核中,LLVM 的 CFI 将 55% 的间接调用限制为最多 5 个可能的目标,80% 限制为最多 20 个目标。
通过链接时优化(LTO)获得完整的程序可见性
为了确定每个间接分支的所有有效调用目标,编译器需要立即查看所有内核代码。传统上,编译器一次处理单个编译单元(源代文件),并将目标文件合并到链接器。LLVM 的 CFI 要求使用 LTO,其编译器为所有 C 编译单元生成特定于 LLVM 的 bitcode,并且 LTO 感知链接器使用 LLVM 后端来组合 bitcode,并将其编译为本机代码。
图 2. LTO 在内核中的工作原理的简单概述。所有 LLVM bitcode 在链接时被组合,优化并生成本机代码。
几十年来,Linux 一直使用 GNU 工具链来汇编,编译和链接内核。虽然我们继续将 GNU 汇编程序用于独立的汇编代码,但 LTO 要求我们切换到 LLVM 的集成汇编程序以进行内联汇编,并将 GNU gold 或 LLVM 自己的 lld 作为链接器。在巨大的软件项目上切换到未经测试的工具链会导致兼容性问题,我们已经在内核版本 4.9 和 4.14 的 arm64 LTO 补丁集中解决了这些问题。
除了使 CFI 成为可能,由于全局优化,LTO 还可以生成更快的代码。但额外的优化通常会导致更大的二进制尺寸,这在资源受限的设备上可能是不需要的。禁用 LTO 特定的优化(比如全局内联和循环展开)可以通过牺牲一些性能收益来减少二进制尺寸。使用 GNU gold 时,可以通过以下方式设置 LDFLAGS 来禁用上述优化:
LDFLAGS += -plugin-opt=-inline-threshold=0 \
-plugin-opt=-unroll-threshold=0
复制代码
注意,禁用单个优化的标志不是稳定 LLVM 接口的一部分,在将来的编译器版本中可能会更改。
在 Linux 内核中实现 CFI
LLVM 的 CFI 实现在每个间接分支之前添加一个检查,以确认目标地址指向一个拥有有效签名的函数。这可以防止一个间接分支跳转到任意代码位置,甚至限制可以调用的函数。由于 C 编译器没有对间接分支强制执行类似限制,函数类型声明不匹配导致了几个 CFI 违规,即使在我们在内核的 CFI 补丁集中解决的内核 4.9 和 4.14 中也是如此。