
Análise técnica e prova de conceito demonstrando um bypass das mitigações do espaço de usuário do kernel Linux para Spectre-BTI via prctl e seccomp, com explicação em nível de código e resultados de teste.
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Ao testar a taxa de sucesso dos ataques Spectre-BTI, detectamos um padrão estranho ao usar a API do kernel como mitigação[^1]. Nossos testes revelaram que o kernel Linux não consegue mitigar corretamente o ataque, deixando o processo exposto por um curto período de tempo após a chamada de sistema.
Investigações adicionais mostraram que o kernel não emite um IBPB imediatamente durante a chamada de sistema. A função ib_prctl_set[^2] atualiza as Flags de Informação de Thread (TIFs) para a tarefa e atualiza o MSR SPEC_CTRL na função __speculation_ctrl_update [^3], mas o IBPB é emitido apenas no próximo agendamento, quando os bits TIF são verificados. Isso deixa a vítima vulnerável a valores já injetados no BTB antes da chamada de sistema prctl. O comportamento só é corrigido após um reagendamento da tarefa. Além disso, a entrada do kernel (devido à própria chamada de sistema) não emite um IBPB nos cenários padrão (ou seja, quando o kernel se protege via retpoline ou eIBRS).
Executar um prctl para mitigar ataques spectre-BTI usando:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
leva à função ib_prctl_set[^2] no kernel 5.15. Quando a opção SPEC_DISABLE é usada, o bit TIF para task_set_spec_ib_disable é definido e task_update_spec_tif é chamado:
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 chama set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); e se a tarefa alvo for a atual, chama 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();
}
A função speculation_ctrl_update_current após o wrapper speculation_ctrl_update executa __speculation_ctrl_update com tifp = ~tifp, aqui a atualização do wrmsr para definir STIBP é executada, mas nenhum IBPB é emitido:
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);
}
A chamada de sistema seccomp também usa ib_prctl_set[^2] como mitigação, dentro de arch_seccomp_spec_mitigate[^4], portanto o mesmo resultado é esperado com seccomp.
Embora por análise de código estejamos certos de que a janela para exploração existe, não estava claro se era grande o suficiente para a vítima carregar segredos e o atacante vazá-los (já que se espera que os segredos não estejam no espaço de endereço da vítima até que a chamada prctl seja emitida). Os testes foram executados em uma máquina bare metal com suporte para mitigações de hardware, com um ubuntu 22.04.1 LTS instalado:
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)
O código de teste consiste em dois processos executando no mesmo núcleo lógico. O atacante envenena constantemente o BTB com o endereço de um gadget spectre presente em um processo vítima. O processo vítima mede a taxa de previsão incorreta verificando se uma variável de teste foi acessada pela função gadget spectre. Isso geralmente retorna a seguinte saída:
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 %)
Em seguida, PRCTL é usado para mitigar o ataque. A mitigação pode ser ativada adicionando prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); no início do programa. Espera-se que isso mitigue o ataque 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 %)
No entanto, alguns dos testes mostraram um resultado diferente:
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 %)
E alterar o 'nice' (prioridade) parece afetar a taxa de previsão incorreta:
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 %)