LWN:LLVM CFI 的新实现方案!
关注了就能看到更多这么棒的文章哦~
A new LLVM CFI implementation
By Jonathan Corbet
June 17, 2022
DeepL assisted translation
https://lwn.net/Articles/898040/
内核中有些功能比起其他部分能存活得更久一些。在 5.13 版内核中加入了对用 LLVM 编译的内核的 forward-edge control-flow integrity(CFI)的完整性检查的支持,但现在已经有一个替代品出现了。CFI
将会被保留下来,但新的实现跟它有很多明显差异,而且在许多方面似乎更好。
内核广泛使用了间接函数调用(indirect function calls);它们是内核里内部对象模型(internal object model)的核心。每一个这种调用,都是攻击者的一个潜在攻击点;如果这个调用的目标地址可以以某种方式被修改为攻击者所选择的地址的话,游戏通常就结束了(我们已经丧失了安全性)。forward-edge CFI 通过确保每个间接函数调用都将控制权发送到实际打算作为该调用目标的代码位置,来挫败这种攻击方式。具体来说,间接函数调用应该只能指向一个已知的函数入口位置(function entry point),并且函数的原型(prototype)应该与调用位置所期望的类型相匹配。
在 5.13 中合并的 CFI 实现是通过创建 "jump tables" 来实现的,这个表中包含了内核里间接函数调用的所有合法目标;针对每一类函数原型,都有一个跳转表。实际的间接调用就会被替换为跳转表的查询操作,以确保预期目标是符合要求的;目标应该是要在与预期函数原型相对应的跳转表中找到的。如果该检查失败了,就会导致内核 panic。关于这个机制如何工作的更详细描述,请看 LWN 之前的文章。
CFI 这么实现了之后可以达到目标,但它也有一些缺点。创建跳转表就需要查看完整的内核二进制文件;在实践中就意味着需要在构建内核的时候使用 link-time optimization,这是一个很耗时、并且有时候有问题的工作。用跳转表项来代替函数指针变量也意味着这些变量就不能跟相应函数的地址来进行比较了,而这是内核代码中有时候确实需要做的动作。如果有一个不存在这类问题的 CFI 实现,那就更好了。
Sami Tolvanen 的这组 patch set 似乎就达到了这个目的。它依赖于一个新的 Clang 编译器选项(-fsanitize=kcfi),这个选项目前还没有进入 LLVM mainline。这种 CFI 机制 "旨在用于底层代码,如操作系统内核上",它就避免了上述问题,但也付出了一些代价,尤其它不能用在那种 execute-only 的内存区域上(因为它必须要能进行 read 访问)。
在用 -fsanitize=kcfi 编译代码时,每个函数的 entry point 前面都有一个代表该函数原型的 32 位值。这个值是由函数及其参数的 C++ 拼写名称(mangled name)计算出来的哈希值(部分)。在 x86 系统中,这个哈希值直接放到了一个简单的 MOV 指令中,并用 INT3 指令包围起来;这是为了防止哈希值本身被攻击者所利用。当进行间接函数调用时,在进行真正的函数调用之前,会先执行额外的代码来获取和检查这个哈希值;如果与预期的不一致就会产生一个 trap(在内核里就是 OOPS)。对哈希值的检查工作就解释了为什么不支持 execute-only 内存:因为必须要能从可执行代码中读取到哈希值。
在大多数情况下,这种机制可以在不需要对内核代码本身做太多改变的情况下就能起效果。至少,比起之前 CFI 实现的那些必须改动来说没有增加其他的了。然而,用汇编编写的函数有些问题,它需要通过其他方式生成这个必需的数值。为每个间接调用的汇编函数都生成一个哈希值可能是一项令人厌烦的任务;幸运的是,编译器提供了一些帮助。每当它看到(在 C 代码中)一个函数的地址被使用时(如这个例子):
static const struct v4l2_file_operations mcam_v4l_fops = {
.open = mcam_v4l_open, /* ... */};
它就会生成一个相应的符号,并将其定义为最终得到的哈希值;在此例中,这个符号就会是 __kcfi_typeid_mcam_v4l_open。存在了这些符号,就意味着汇编函数的前面的那个数值就可以通过对已经用于定义这些函数的宏进行一些调整而自动生成了。
这组 patch 目前处于第三版,似乎所有的实质性问题都已经解决。换句话说,它看起来已经准备好被并入 mainline 了。只剩下一个障碍需要克服:在 LLVM Clang 编译器真正支持这个功能之前,内核开发者不会愿意合并这个功能。假设 Clang 不久之后就能支持这个功能的话,那么在内核应该很快就会获得 arm64 和 x86 架构的升级版 CFI 实现了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~