
Análisis técnico y prueba de concepto que demuestra una evasión de las mitigaciones de Spectre-BTI del kernel de Linux en el espacio de usuario mediante prctl y seccomp, con explicación a nivel de código y resultados de pruebas.
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Al probar la tasa de éxito de los ataques Spectre-BTI, detectamos un patrón extraño al usar la API del kernel como mitigación[^1]. Nuestras pruebas revelaron que el kernel de Linux no mitiga correctamente el ataque, dejando el proceso expuesto durante un breve período de tiempo después de la syscall.
Una investigación más a fondo mostró que el kernel no emite un IBPB inmediatamente durante la syscall. La función ib_prctl_set[^2] actualiza los indicadores de información de hilo (TIF) de la tarea y actualiza el MSR SPEC_CTRL en la función __speculation_ctrl_update [^3], pero el IBPB solo se emite en la siguiente planificación, cuando se comprueban los bits TIF. Esto deja a la víctima vulnerable a valores ya inyectados en el BTB antes de la syscall prctl. El comportamiento solo se corrige después de que ocurra una replanificación de la tarea. Además, la entrada al kernel (debido a la propia syscall) no emite un IBPB en los escenarios predeterminados (es decir, cuando el kernel se protege mediante retpoline o eIBRS).
Ejecutar un prctl para mitigar ataques Spectre-BTI usando:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
conduce a la función ib_prctl_set[^2] en el kernel 5.15. Cuando se usa la opción SPEC_DISABLE, se establece el bit TIF para task_set_spec_ib_disable y se llama a 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 llama a set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); y si la tarea objetivo es la actual, llama a 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();
}
Tras el envoltorio speculation_ctrl_update, speculation_ctrl_update_current ejecuta __speculation_ctrl_update con tifp = ~tifp; aquí se ejecuta la actualización del wrmsr para establecer STIBP, pero no se emite ningún 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);
}
La syscall seccomp también usa ib_prctl_set[^2] como mitigación, dentro de arch_seccomp_spec_mitigate[^4], por lo que se espera el mismo resultado con seccomp.
Si bien mediante el análisis del código estamos seguros de que la ventana para explotar existe, no estaba claro si era lo suficientemente grande como para que la víctima cargara secretos y el atacante los filtrara (ya que se espera que los secretos no estén en el espacio de direcciones de la víctima hasta que se realiza la llamada prctl). Las pruebas se ejecutaron en una máquina bare metal con soporte para mitigaciones por hardware, con 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)
El código de prueba consta de dos procesos que se ejecutan en el mismo núcleo lógico. El atacante envenena constantemente el BTB con la dirección de un gadget de Spectre presente en un proceso víctima. El proceso víctima mide la tasa de predicciones incorrectas comprobando si una variable de prueba fue accedida por la función del gadget de Spectre. Esto normalmente devuelve la siguiente salida:
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 %)
A continuación, se usa PRCTL para mitigar el ataque. La mitigación se puede habilitar añadiendo prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); al comienzo del programa. Se espera que esto mitigue el 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 %)
Sin embargo, algunas de las pruebas mostraron un 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 %)
Además, cambiar la prioridad ('nice') parece afectar la tasa de predicciones incorrectas:
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 %)