LWN:Intel CPU的间接跳转保护功能!

Linux News搬运工

共 3019字,需浏览 7分钟

 ·

2022-05-14 10:21

关注了就能看到更多这么棒的文章哦~

Indirect branch tracking for Intel CPUs

By Jonathan Corbet
March 31, 2022
DeepL assisted translation
https://lwn.net/Articles/889475/

"Control-flow integrity"(CFI)是一套旨在防止攻击者改动程序的控制流(control flow)并接管它的技术。CFI 采用的方法之一被称为 "间接跳转跟踪"(IBT, indirect branch tracking),希望能防止攻击者让间接跳转(例如,通过指针变量进行的函数调用)进入一个不应该走到的地方。英特尔处理器的 IBT 功能已经开发了一段时间了。经历了一个突然的转折之后,用 IBT 保护内核的功能已经被合并到即将发布的 5.18 版本中。

内核也跟许多 C 程序一样,在广泛使用 indirect branch 间接跳转。举一个简单的例子,来讨论一下系统调用:用户空间提供了一个数字,表明需要调用哪个系统调用,内核通过从表中查找到相应的函数(利用该数字编号)并通过 indirect branch 来调用该函数进行处理。函数指针在内核中比比皆是,其中之一的用途就是用来实现内核中类似面向对象的编程模型。

如果攻击者能够以某种方式破坏一个用于 indirect branch 的变量,那么他们可能能够将内核的执行流程(control flow)重定向到任意位置。这可能会导致意料之外的函数被调用了。在像 x86 这样复杂的处理器上,也有可能通过跳转到一条多字节指令的中间位置来获得特殊效果。有一些攻击技术如 return-oriented programming 或者 jump-oriented programming 就依赖这个功能。

IBT 是为了防御面向跳转编程的(jump-oriented programming);工作原理是试图确保每个 indirect branch 的目标确实都是适合作为跳转目标的。IBT 的方法有很多,每一种都有自己的优势和劣势。例如,内核在 5.13 开发周期中支持了编译器实现的 IBT 机制。在这种模式下,编译器通过一个 "jump table, 跳转表" 来完成每一个间接跳转,不仅确保目标是要供间接跳转使用的,而且要确保被调用函数的原型与调用者所期望的一致。这种方法是很有效的,但要增加很多编译、运行时的开销。

Intel's IBT

英特尔的 IBT 方法相当简单,但优点是得到了硬件的支持,因此速度更快。如果 IBT 被启用,那么 CPU 将确保每个间接跳转都落在一条特殊指令(endbr32 或 endbr64)上,该指令执行时跟 no-op 效果一致。如果发现意外,那么处理器将引发一次 control-protection(#CP)exception。与上面描述的更完整的方案不同,IBT 不能确保间接跳转的目标与调用者的期望一致,但它可以确保目标是同意以间接跳转方式调用的。

只有当间接跳转的每个可能目标地址都以 endbr 这种指令开始时,才有开启这种硬件保护机制的必要。大多数情况下,这项任务可以由编译器完成;GCC(从 GCC 9 开始)和 Clang(从 14 版开始)都实现了-fcf-protection=branch 选项,会在适当的时候插入插入 endbr 这类指令。不过,这对内核中现存的所有汇编代码并没有帮助。所以这个 patch set 中大部分的工作都是在需要的地方手动加入 endbr 指令。

当内核调用别人的代码时,还有一个小麻烦,这些代码可能没有考虑到 IBT。内核并不经常调用外部代码,但有一个很大的例外就是系统的固件(firmware),它必须经常被调用来执行特定的功能。为了安全起见,内核会在调用固件时关闭 IBT。目前的实现也在将控制权交给用户空间时关闭 IBT。

需要在所有的间接跳转目标地址都添加 endbr 指令,这就为今后造成了一个潜在的陷阱:开发者可能会添加汇编函数但是忘记了这个指令。如果他们在没有启用 IBT 的情况下进行测试,那么这个问题就不会被注意到,可能直到有问题的代码被合并后的某个让人非常不爽的时间点才突然暴露出来。为了防止这种情况的发生,开发者加强了内核的 objtool 工具来检查所有的间接跳转,并确保所有的目标都恰当地标注(annotated)好了。

不过,有了这种检查之后,还可以采取另一个步骤:objtool 还可以列出所有包含 endbr 指令但是永远不会通过间接跳转到达的函数。这些函数不需要进行标注,没有它们的话,内核会更安全一些。因此,内核 build 的过程会从 objtool 中获取该列表,并通过用 nop4 指令覆盖 endbr 来 "封印" 这些函数。这就减少了攻击者在启用 IBT 时仍然可以选择的攻击目标。

正如 Peter Zijlstra 所指出的,删除不需要的 endbr 指令还有一个也许是令人惊讶的好处。内核限制了 loadable module 所能使用的函数,而那些专有的(proprietary)module 则受限更多。那些专有的 module 经常使用一个技术就是在内核的符号表中查找到它们需要的、未 export 的函数,然后通过间接跳转来调用它们,从而绕过内核的限制。但是,在启用 IBT 后,任何缺乏 endbr 指令的函数都不再能以这种方式调用了。

An indirect path to the mainline

开发者已经努力了一段时间,希望能让英特尔 IBT 功能进入 Linux 内核。实现这个功能的第一个 patch(针对用户空间代码而不是针对内核)是由 Yu-cheng Yu 在 2018 年发布的。然后,这项工作似乎成了那些不断穿越邮件列表的飞行荷兰人(flying-Dutchman)patch 之一,从未能够进入 mainline;第 30 版是 2021 年 8 月发布的,也没有达到能够合并的程度。类似的情况也发生在了用户空间的影子堆栈(user-space shadow-stack)patch set 上,在经历多次修订后,这些 patch 最近被 Rick Edgecombe 接手了。

去年年底,Peter Zijlstra 决定创建一个独立的英特尔 IBT 实现来保护内核本身;第一个版是在去年 11 月发布的,Zijlstra 显然是 "在周五晚上/周六早上赶工出来的"。这项工作发展得很快,3月初发布的第四个修订版就是为 5.18 合并的版本了。

这就是目前为止的情况了。从 2020 年底上市的 Tiger Lake 一代开始,英特尔处理器就支持 IBT 了,只支持了内核代码。这并不是一个完美的工具,但它将提高攻击者攻击这类系统的门槛。同时,目前还不清楚用户空间的支持何时(或是否)会进入 mainline 内核;到目前为止发布的 30 个修订版中,有许多根本没有收到任何 review 意见。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 7
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报