LWN:GCC 开始支持内核控制流完整性

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

Daroc Alden
 Gemini translation
 原文链接:https://lwn.net/Articles/1056601/ 

控制流完整性 (Control-flow integrity, CFI) 是一套技术方案,旨在增加攻击者通过劫持间接跳转 (indirect jumps) 来利用系统的难度。Linux 内核自 2020 年 起就开始支持前向控制流完整性 (Forward-edge CFI,用于保护间接函数调用),而该功能的最新实现方案是在 2022 年 引入的。这一版本通过使用 Clang 中提供但 GCC 中尚未支持的编译器标志 -fsanitize=kcfi 避免了早期方法带来的开销。现在,Kees Cook 提交了 一套补丁集,为 GCC 添加了该支持,目前看来很有可能进入 GCC 17。 

CFI 需要解决一个棘手的问题:程序只应执行开发者预期的间接函数调用。如果程序中没有漏洞,这本该很简单——相关的函数指针始终是正确的,也就无需担心任何问题。然而,内核并非没有漏洞,攻击者总是有可能成功将函数指针覆盖为他们控制的某个值。当涉及的函数指针可能被破坏时,编译器该如何防御不正确的函数调用呢? 

内核的前向控制流完整性所采取的方法是根据函数的类型签名 (Type signature) 对函数进行分类。例如,如果某段代码期望调用一个接收 long 类型参数的函数,但实际的函数指针却指向一个接收 char * 参数的函数,这就表明某些地方出错了。通过识别这种类型混淆 (Type confusion) 并让内核进入恐慌 (panic) 状态,前向控制流完整性可以阻止某些攻击。这并非完美的解决方案:只要函数签名匹配,攻击者仍然可以重定向间接函数调用。不过,这仍然比完全没有保护要好。 

这种缓解措施是 Linux 内核自保护项目 (Linux kernel self-protection project, KSPP) 的典型工作,Cook 正是以该项目的名义开展这项工作的。该项目基于这样一个现实:内核中的许多漏洞都有很长的漏洞生命周期 (Bug lifetime)。LWN 最近对 6.17 内核的漏洞生命周期进行了 深入分析 (显示出,在已知存在于特定内核版本的漏洞中,通常需要数年时间才能发现其中的大部分)。LWN 订阅者可以在 LWN 内核源码数据库 中查看任何内核版本的详细信息。鉴于漏洞往往会长期存在,内核自保护项目支持研究各种技术,以便在存在这些漏洞的情况下仍能保持内核的安全。该项目在其 GitHub 问题追踪器 上跟踪正在进行的工作。 

Clang 通过计算函数类型的哈希 (Hash) 值,并将其存储在内存中函数实现的前面来实现 -fsanitize=kcfi。当代码中的调用点执行间接调用时,它会首先验证哈希值是否与预期匹配。GCC 计划以同样的方式实现该功能,但遗憾的是,添加支持并不像看起来那么简单。自 2025 年 8 月的初始版本 以来,该补丁集已经经历了九个版本的迭代。 

一个复杂之处在于对类型别名 (Type aliases) 的处理:在 C 代码中,为了提高可读性,经常会使用类型别名,如果为指向同一底层类型的两个类型别名生成不同的哈希值,将会破坏现有的内核代码。因此,在哈希过程中,大多数类型别名都会被解析并替换为底层类型。但在 C 代码中,有一种情况下的类型别名通常是有意义的:作为原本匿名的结构体、联合体和枚举类型的名称。如果两个匿名联合体恰好具有相同的字段,但它们的命名不同,程序员几乎总是希望确保其中一个不会被误认为是另一个。在这种情况下,编译器会将类型别名名称作为哈希值的一部分。 

●●●
..// 这被视为与普通的 int 相同:
..typedef.int.port_number;

..// 但这两个类型被认为是不同的:
..typedef.struct.{
....void.*data;
..}.foo;

..typedef.struct.{
....void.*data;
..}.bar;

另一个复杂之处源于对 LLVM 兼容性的需求。过去,内核编译理论上是完全使用 GCC 或完全使用 Clang 完成的。实际上,Cook 希望为那些 C 代码由 GCC 编译、而 Rust 代码由 rustc (使用 LLVM 进行代码生成) 编译的内核支持 CFI。最终,一旦 GCC 的 Rust 前端可以工作,这可能就不再必要了。但就目前而言,执行指向 Rust 代码的间接函数调用的 C 代码(反之亦然)需要计算与另一种语言相同的类型哈希,以便在运行时数值能够匹配。Clang 的类型哈希基于 Itanium C++ ABI 所要求的函数名修饰 (Name-mangling);Cook 为 GCC 实现了相同的算法。Sami Tolvanen 似乎没有记录 为什么为 LLVM 的原始实现选择了 Itanium ABI,但这可能是因为 Itanium ABI 是 POSIX 系统中唯一实际指定了名称修饰规则,而不是将其留给编译器自行决定的 ABI 标准。 

相关的哈希值需要在每个调用点进行计算,但也需要为每个可能被间接调用的函数进行计算。由于 C 文件是独立编译的,因此无法知道某个被取地址的函数是否真的被调用了。为了安全起见,GCC 会为每个被取地址的函数,以及每个对其他编译单元 (Translation units) 直接可见的函数计算并嵌入哈希值。 

检查类型哈希是否匹配的实际工作是在编译器流水线的后期实现的,以确保它们不会被优化掉或以其他方式被拆散。因此,需要一些特定于架构的代码来实现这些检查。Cook 的补丁集包括了对 x86_64、32 位和 64 位 Arm 以及 RISC-V 的支持。 

虽然早期版本引起了大量讨论,但当前版本并未吸引 GCC 开发者太多的评论。Jeffrey Law 认为,一旦 GCC 16 的最后一个版本发布,该补丁集就准备好进入 GCC 17 了。尽管支持尚未正式落地 GCC,但内核在 6.18 版本中 已经将相关的配置选项从 CONFIG_CFI_CLANG 重命名为 CONFIG_CFI。该配置选项使用功能探测,因此即使名称发生了变化,旧的稳定版内核也将通过 GCC 17 支持该选项。 

然而,仍有更多工作要做。例如,2022 年曾有过 一场讨论,关于在 CFI 哈希中添加由程序员指定的每个函数种子 (per-function seed) 到 CFI 哈希中的可能性。那将允许程序员手动将具有相同签名的函数集划分为具有不同 CFI 哈希值的不同组。Bill Wendling 和 Aaron Ballman 在 2025 年 8 月 为 LLVM 添加了 对此的支持,尽管内核尚未利用它。 

内核安全的进步并非一蹴而就。类似 CHERI 的硬件强制能力可以用来完全阻止基于间接跳转的攻击。现代 CPU 正越来越多地添加对 CFI 操作的硬件支持。对于旧设备,像 Clang 的 CFI 支持这样的软件修复方案有助于填补空白。现在,GCC 将为那些出于兼容性或意识形态原因选择使用 GCC 的用户带来同样的 CFI 方案。最终,也许这种缓解措施将直接成为默认设置。 

LWN 评论概述:

一位读者好奇在静态作用域中采用索引加开关 (index+switch) 的方案是否会产生过高的性能影响,这种方案可以避免抑制优化,并可能在内核中实现去虚拟化 (devirtualization),尽管在处理独立编译的文件和加载模块时可能需要一些“链接器魔法”。另一位读者询问这是否是一种不需要 IBT 等硬件支持的纯软件前向控制流完整性实现,并关心相关的性能基准测试。还有读者指出文章对 CHERI 的描述不准确,强调 CHERI 与控制流跳转无关,而是通过向指针添加元数据来防止越界访问。 

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值