
Un interrupt molto molto molto molto molto molto molto lungo
Sfruttare la System Management Mode con un interrupt molto molto molto molto molto molto molto lungo.
Si scopre che puoi rompere la SMM — l'ambiente di esecuzione sicuro e ultra privilegiato che gira invisibilmente in background su ogni CPU x86 — con niente più di un'istruzione macchina oscenamente lunga.
La SMM richiede che tutti i core siano o nella SMM o fuori dalla SMM allo stesso tempo. Il suo modello di sicurezza non funziona senza questo — quando un thread entra nella SMM, costringe anche tutti gli altri a entrarci.
Per rompere questo, tutto ciò di cui abbiamo bisogno è qualcuno troppo occupato per accorgersi che dovrebbe unirsi alla SMM.
Funziona più o meno così:
core 0 - avvia un'istruzione lunga
|
|
|
core 1 - invita il core 0 alla smm
|
|
|
core 1 - entra nella smm
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - aspetta il core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - molla
core 1 - fa cose segrete nella smm
core 1 - termina la smm
|
|
|
core 0 - entra nella smm
A questo punto, il core 1 è fuori dalla SMM mentre il core 0 è dentro, permettendo al core 1 di attaccare
il core 0. Ecco il problema: perché questo funzioni abbiamo bisogno di un'istruzione molto, molto, mooolto
lunga — più lunga di quanto qualsiasi istruzione dovesse mai durare. La maggior parte delle
istruzioni macchina su una CPU moderna sono veloci: add richiede 1 ciclo. Per far sì che il core
1 smetta di aspettare il core 0, abbiamo bisogno di un'istruzione sul core 0 che richieda
circa 4.000.000.000 di cicli — oltre 1 secondo di tempo reale.
Il firmware x86 esegue il seguente codice quando un core della CPU entra nella SMM:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
Il codice attende che tutti i core entrino nella SMM, o fino a 1 secondo, a seconda di quale si verifichi per primo. Per far eseguire a un core il codice SMM mentre un altro core continua a eseguire fuori dalla SMM, abbiamo bisogno che quel core esterno rimanga non interruptibile per l'intero secondo — un SMI viene catturato al confine di un'istruzione, quindi qualsiasi intervallo tra due istruzioni permette all'SMI pendente di trascinare il core nella SMM. Il ritardo quindi deve essere una singola istruzione: un'operazione non interruptibile che sopravviva al rendezvous di un secondo.
Ci sono molti modi per raggiungere la proibita istruzione da 1 secondo, e l'approccio esatto varierà da piattaforma a piattaforma. Ma, in linea di massima: trova un indirizzo MMIO ad alta latenza, e poi convinci la CPU a leggerlo il più lentamente possibile — abusa di una regione non documentata che risponde alle letture al rallentatore, usa il load più largo che l'ISA ti offre per spostare quanti più byte possibile attraverso di essa in una singola istruzione, e lascia che gli altri core competano per lo stesso bus per rallentarla ulteriormente. Una lettura, un'istruzione, e la CPU rimane bloccata a mantenerla per la maggior parte di un secondo.
Il proof-of-concept fornito
è ottimizzato per un Zen 3 Ryzen 7
5800H, dove un load xmm largo da MMIO lento a
0xfcc68860 si blocca abbastanza a lungo da rompere il rendezvous di tutti i core:
mov $0xfcc68860, %rsi ; l'indirizzo MMIO di destinazione
vmovdqu (%rsi), %xmm0 ; il load molto, molto lungo
Il PoC sfrutta questo mettendo due core l'uno contro l'altro. Un core viene tenuto fuori dalla SMM dall'istruzione lunga — un loop stretto sul load molto lento:
/* il core vittima: gira sul load da ~1 secondo, troppo occupato per rispondere all'SMI */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
Nel frattempo un altro core arma i contatori SMI per-core:
#define MSR_PERF_CTL0 0xc0010200 /* MSR di selezione evento perf core AMD */
#define MSR_PERF_CTR0 0xc0010201 /* il contatore a 48 bit abbinato */
for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
msr_write(cpu, MSR_PERF_CTL0, 0x43002b); /* EN | OS | USR | evento 0x2b */
msr_write(cpu, MSR_PERF_CTR0, 0); /* azzera il conteggio */
}
Poi scatena una tempesta di SMI:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* calcia la porta 0xb2 -> #SMI */
E legge il totale di ogni core:
/* ...scatena la tempesta, poi leggi il totale di ogni core... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! un core è girato fuori dalla SMM");
Se i conteggi divergono, significa che un core ha continuato a girare fuori dalla SMM mentre gli altri venivano trascinati dentro — ha mancato gli SMI che gli altri hanno servito.
Per illustrare meglio questo, possiamo eseguire il proof-of-concept dietro una GUI inutilmente appariscente e del tutto inutile, tracciando i contatori SMI su ogni core, per guardarli eseguire in perfetto sincronismo finché un core estremamente ritardato rompe la loro sincronizzazione richiesta:

L'unica garanzia della SMM — che nient'altro giri mentre lei è in esecuzione — crolla davanti a una singola istruzione assurda e lunga.
La sicurezza della SMM si basa su un'assunzione semplice: mentre è in esecuzione, nient'altro lo è.
Ci sono 100+ CVE TOCTOU della SMM in giro: un handler della SMM controlla un valore nella memoria condivisa, poi lo usa. Tutto ciò di cui hai bisogno per lo sfruttamento è riscrivere quel valore tra il controllo e l'uso, e sei dentro la SMM. Ma questi problemi restano dormienti e in gran parte non patchati nel mondo reale, a causa di un'assunzione: lo sfruttamento richiede qualcosa che modifichi la memoria condivisa mentre la SMM è in esecuzione, e a causa del rendezvous della SMM nessun core della CPU è fuori dalla SMM per lanciare un attacco. L'unico modo per entrare era una periferica con capacità DMA che scrivesse alle spalle della CPU — accesso fisico, un dispositivo dannoso — quindi l'intera classe viene liquidata come un problema hardware.
La desincronizzazione degli SMI rimuove il prerequisito che manteneva sicura la piattaforma: un core esterno, senza accesso fisico o hardware richiesto, può ora girare mentre la SMM è in esecuzione — e le CVE dormienti diventano sfruttabili dal software.
Probabilmente non ce ne sono, ed è questo che rende questo problema un po' più interessante dei problemi SMM tradizionali. Mantieni il timeout, e il rendezvous viene facilmente rotto. Rimuovi il timeout, e un core legittimamente bloccato appende la piattaforma al primo SMI. Aumenta il timeout, e uccidi le prestazioni sulle piattaforme multi-core che sono costrette a mettere a riposo tutti i core a ogni ingresso nella SMM. Non è chiaro quale sia la strada migliore da seguire, o se esista anche solo una strada da seguire.
Fino ad allora, la soluzione consigliata è di non eseguire alcuna istruzione lunga.
Il vmovdqu predefinito a 0xfcc68860 nel proof-of-concept è un punto lento su
questa macchina — un Zen 3 Ryzen 7 5800H — e probabilmente da nessun'altra parte. Per rompere il
rendezvous sulla tua macchina, dovrai riottimizzare l'istruzione lunga così che
lo stallo sopravviva al tuo timeout SMM. Alcuni suggerimenti su come fare:
-r xmm → ymm → zmm finché lo stallo supera il
timeout del rendezvous.make # compila smiiiiiiiiiiiiiiii
Le impostazioni predefinite sono ottimizzate per una sola macchina. Su qualsiasi cosa tranne un Zen 3 Ryzen 7 5800H, non aspettarti divergenze finché non riottimizzi l'istruzione lunga — vedi Porting sulla tua piattaforma.
Esegui lo strumento per scatenare ripetutamente l'istruzione molto-molto-lunga mentre osservi il contatore SMI di ogni core per una divergenza:
sudo ./smiiiiiiiiiiiiiiii # predefinito: -r xmm a 0xfcc68860
Flag:
| Flag | Predefinito | Descrizione |
|---|---|---|
-r xmm|ymm|zmm | xmm | Larghezza del registro vettoriale per la lettura MMIO temporizzata (16/32/64 byte). Se non viene osservato alcun delta nel conteggio SMI, lo strumento consiglia di passare alla dimensione successiva. |
-a <phys-addr> | 0xfcc68860 | Indirizzo fisico di destinazione per il loop di temporizzazione MMIO (esadecimale 0x... o decimale). |
-h, --help | — | Stampa l'utilizzo ed esce. |
smiiiiiiiiiiiiiiii è uno sforzo di ricerca di Christopher Domas (@xoreaxeaxeax).