LWN:FineIBT保护仍有漏洞!

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

A hole in FineIBT protection

By Jonathan Corbet
February 27, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1011680/

英特尔的 间接分支追踪 (indirect branch tracking, IBT) 是一种硬件实现的控制流完整性(control-flow-integrity)机制,它可以防止攻击者通过破坏间接分支(indirect branch)来获得系统控制权。FineIBT 是 IBT 的一个软件扩展,旨在提高其保护能力。然而,最近 Jennifer Miller 报告了一种绕过 FineIBT 的新方法,该方法利用了内核的系统调用入口点的构造方式。作为回应,Peter Zijlstra 正在研究 一些 FineIBT 增强功能,以填补该漏洞并提高 IBT 的整体安全性。

内核(许多其他程序也一样)广泛使用间接分支,通常是以使用在运行时确定的指针值进行函数调用的形式。这些间接调用一直是攻击者青睐的目标;如果该指针可以被设置为攻击者控制的值,则调用可以被发送到任意位置。通常,这就足以获得系统控制权了。在像内核这样庞大的代码中,肯定会存在一个指令序列,当以意外的方式调用时,该序列会为攻击者执行一些有用的操作。

IBT 通过阻止间接调用跳转到任意位置来实现前向边缘控制流完整性(forward-edge control-flow integrity)。它的工作原理是要求间接调用的目标必须是一个特殊的指令,即 endbr32 或 endbr64 (统称为 endbr ), 它作为标记指示合法的调用目标。IBT 大大减少了间接调用可以跳转的位置;调用只能跳转到实际的函数入口点,而不是程序中的任何位置(包括多字节指令的中间)。

这种限制提高了安全性,但提高的程度有限。像内核这样的程序中仍然存在很多函数,并且通过将控制流重定向到意外的函数可能会造成很大的破坏。如果 IBT 可以确保间接调用落在预期的目标上,而不是任何函数上,那么保护将会更加严格。FineIBT 在 2023 年的 这篇论文 中有所描述,是朝着这个方向迈出的一步。在内核的实现中,每个间接调用都被修改为首先将一个特殊的哈希值加载到特定的寄存器中。被调用的函数,在 endbr 指令之后,将立即将该哈希值与期望的值进行比较;如果两者不匹配,则执行将被中止。哈希值是从被调用函数的原型生成的,但随后在启动时会被扰乱,因此任何给定运行系统中使用哈希值都是不同的(并且希望攻击者不知道)。

FineIBT 已合并到 6.2 内核中,并且自那以后,有望使攻击者的生活更加艰难。正如 Miller 所证明的那样,这种保护并不是绝对的。在这种情况下,绕过 FineIBT 的方法利用了内核中的一个特殊的汇编语言函数,即 entry_SYSCALL_64(),当用户空间发出系统调用时,CPU(在 x86_64 系统上)会调用该函数。查看代码,可以看到它以预期的 endbr 指令开始;IBT 要求即使在响应系统调用陷阱时也必须如此。

但是,后面的指令不是通常的 FineIBT 哈希验证,因为该函数不会从内核内部调用。取而代之的是 swapgs ,它将处理器的 GS 段基址寄存器的内容与特殊型号特定寄存器 (model-specific register, MSR) 的内容交换。需要此指令,因为在进入内核时,尚未设置内核的执行环境,因此无法访问内存(或执行任何操作)。执行 swapgs 是建立该环境的第一步,从而允许访问内核数据和内核堆栈。紧接在返回到用户空间之前,内核将执行另一个 swapgs 以将 GS 基址寄存器恢复为其用户空间值。

如果攻击者能够将间接分支重定向到 entry_SYSCALL_64() ,那么硬件 IBT 检查将会通过,因为存在预期的 endbr 指令。但是,FineIBT 哈希检查不会发生,因为该代码在该函数前导码中不存在。结果,将允许对该函数的恶意间接调用继续执行。这已经够糟糕了,但是 swapgs 指令使情况变得更糟。它将恢复用户空间 GS 基址值(首次进入内核时替换的值),而 CPU 仍在内核模式下运行;允许用户空间更改该寄存器,因此内核的 GS 基址将被设置为完全在攻击者控制下的值。除此之外,这还会使内核堆栈受到攻击者的控制;结果是快速接管内核。

当以这种方式阐明时,问题是相当明显的;仅当每个函数都包含该检查时,在被调用函数中检查哈希值才有效 — 并且存在一些函数,包括 entry_SYSCALL_64() ,无法执行检查。将检查移至调用方可以避免此问题,但代价是使整个序列的成本略高。这就是 Zijlstra 采取的方法;用于此检查的代码序列,位于 此补丁 中,值得一看:

/* * Notably LEA does not modify flags and can be reordered with the CMP, * avoiding a dependency. Again, using a non-taken (backwards) branch * for the failure case, abusing LEA's immediate 0xf0 as LOCK prefix for the * Jcc.d8, causing #UD. */asm(    ".pushsection .rodata           \n"    "fineibt_paranoid_start:            \n"    "   movl    $0x12345678, %r10d      \n"    "   cmpl    -9(%r11), %r10d         \n"    "   lea -0x10(%r11), %r11       \n"    "   jne fineibt_paranoid_start    0xd   \n"    "fineibt_paranoid_ind:              \n"    "   call    *%r11               \n"    "   nop                 \n"    "fineibt_paranoid_end:              \n"    ".popsection                    \n");

0x12345678 在运行时使用期望的哈希值进行修补。当需要执行间接调用时, cmpl 指令将修补后的值与期望存储在间接调用函数的入口点之前的值进行比较。=lea= 指令本质上是一个快速的空操作 (no-op),但它的存在是为了以下巧妙之处。 jne 指令将查看两个指令之前的 cmpl 的结果;在不相等的情况下(哈希值不匹配),它会向后跳转到刚执行的代码中;否则,将像往常一样执行 call 。

为什么要向后跳转?由于未采用的分支比采用的分支更快,因此在不常见的情况下最好跳转。但是,此特定跳转值得注意,因为它将落在 lea 指令 中间,这将导致 CPU 看到无效的指令序列并生成 #UD 陷阱。似乎,这种技巧是能找到的最快,最节省空间的方式来执行测试并生成陷阱,而不会不必要地减慢合法的间接函数调用(应该是所有这些调用)。显然,此特殊序列是英特尔的 Scott Constable 的创意;在补丁集的 先前版本 中,Zijlstra 告诫说:“请注意,Scott 喜欢重叠指令”。

在此补丁系列完成时,精心记录不足的 cfi 命令行参数有几个新选项。设置 cfi=warn 会导致控制流完整性错误生成警告而不是生成 oops,而 cfi=paranoid 启用新的调用前验证模式。在本系列的结尾,还有一个 补丁 添加了另一个选项 cfi=bhi ,它可以改善 Spectre 缓解措施,这些缓解措施应该内置于 IBT 中,但在某些处理器中发现硬件级别上有所欠缺。

Zijlstra 在 cover letter 中表示希望当前版本的补丁集是合并前的最后一个版本。这种希望在内核世界中经常破灭,但是此系列似乎正接近完成。目前尚不清楚攻击者是否曾利用 Miller 报告的绕过方法,但是一旦此代码生效,任何此类漏洞利用的作者都将不得不寻找一种新的方法来绕过内核的控制流完整性保护。

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值