
Une très très très très très très très longue interruption
Exploiter le mode de gestion du système avec une interruption très très très très très très très longue.
Il s'avère que l'on peut briser le SMM — l'environnement d'exécution sécurisé et ultra privilégié qui tourne invisiblement en arrière-plan de chaque CPU x86 — avec rien de plus qu'une instruction machine absurdement longue.
Le SMM exige que tous les cœurs soient soit dans le SMM, soit hors du SMM en même temps. Son modèle de sécurité ne fonctionne pas sans cela - lorsqu'un thread entre dans le SMM, il fait entrer tous les autres aussi.
Pour briser cela, tout ce qu'il faut, c'est quelqu'un de trop occupé pour remarquer qu'il est censé rejoindre le SMM.
Cela fonctionne à peu près comme ceci :
core 0 - start a long instruction
|
|
|
core 1 - invite core 0 to smm
|
|
|
core 1 - enter smm
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - wait for core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - give up
core 1 - do secret smm stuff
core 1 - finish smm
|
|
|
core 0 - join smm
À ce stade, le cœur 1 est hors du SMM pendant que le cœur 0 y est, permettant au cœur 1 d'attaquer le cœur 0. Voici le hic : pour que cela fonctionne, nous avons besoin d'une instruction très, très, trrrrès longue — plus longue que n'importe quelle instruction n'aurait jamais dû prendre. La plupart des instructions machine sur un CPU moderne sont rapides : add prend 1 cycle. Pour que le cœur 1 abandonne l'attente du cœur 0, il nous faut une instruction sur le cœur 0 qui prenne environ 4 000 000 000 de cycles — plus d'une seconde de temps réel.
Le firmware x86 exécute le code suivant lorsqu'un cœur de CPU entre dans le SMM :
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
Le code attend que tous les cœurs entrent dans le SMM, ou jusqu'à 1 seconde, selon ce qui survient en premier. Pour qu'un cœur exécute du code SMM pendant qu'un autre cœur continue de s'exécuter hors du SMM, il faut que ce cœur extérieur reste ininterruptible pendant toute la seconde — un SMI est pris à une frontière d'instruction, donc tout écart entre deux instructions est une porte par laquelle le SMI en attente passe pour entraîner le cœur. Le délai doit donc être une seule instruction : une opération ininterruptible qui survit au rendez-vous d'une seconde.
Il existe de nombreuses façons d'atteindre l'instruction interdite d'une seconde, et l'approche exacte varie d'une plateforme à l'autre. Mais, en gros : trouvez une adresse MMIO à haute latence, puis convainquez le CPU de la lire aussi lentement que possible — exploitez une région non documentée qui répond aux lectures au ralenti, puis utilisez la charge la plus large que l'ISA vous offre pour transporter un tas ridicule d'octets en une seule instruction, idéalement pendant que les autres cœurs se bousculent sur le même bus pour que l'ensemble peine sous la contention. Une lecture, une instruction, et le CPU reste coincé dessus pendant la majeure partie d'une seconde.
La preuve de concept fournie est réglée pour un Zen 3 Ryzen 7 5800H, où une large charge xmm depuis une MMIO lente à 0xfcc68860 se fige assez longtemps pour briser le rendez-vous de tous les cœurs :
mov $0xfcc68860, %rsi ; the target MMIO address
vmovdqu (%rsi), %xmm0 ; the very, very long load
La PoC exploite cela en opposant deux cœurs. Un cœur est maintenu hors du SMM par l'instruction longue — une boucle serrée sur la charge très lente :
/* the victim core: spin on the ~1-second load, too busy to answer the SMI */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
Pendant ce temps, un autre cœur arme les compteurs SMI par cœur :
#define MSR_PERF_CTL0 0xc0010200 /* AMD core perf event-select MSR */
#define MSR_PERF_CTR0 0xc0010201 /* the paired 48-bit counter */
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); /* zero the count */
}
Puis déclenche une tempête de SMI :
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* kick port 0xb2 -> #SMI */
Et relit le total de chaque cœur :
/* ...fire the storm, then read every core's tally back... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! a core ran outside SMM");
Les compteurs racontent l'histoire. S'ils divergent, un cœur a continué de tourner hors du SMM pendant que les autres étaient aspirés — il a survécu aux SMI que les autres ont traités sans lui.

Et c'est tout le résultat : l'unique promesse du SMM, selon laquelle rien d'autre ne s'exécute pendant qu'il s'exécute, s'effondre face à une instruction absurdement longue.
La sécurité du SMM repose sur une hypothèse simple : pendant qu'il s'exécute, rien d'autre ne le fait.
Il existe 100+ CVE SMM TOCTOU par là : un gestionnaire SMM vérifie une valeur dans la mémoire partagée, puis utilise cela. Tout ce qu'il faut pour l'exploitation, c'est réécrire cette valeur entre la vérification et l'utilisation, et vous êtes dans le SMM. Mais ces problèmes sont dormants et en grande partie non corrigés dans la nature, à cause d'une hypothèse : l'exploitation exige que quelque chose modifie la mémoire partagée pendant que le SMM s'exécute, et à cause du rendez-vous SMM, aucun cœur de CPU n'est hors du SMM pour lancer une attaque. La seule voie d'entrée — du moins le croyions-nous — était un périphérique capable de DMA écrivant dans le dos du CPU — accès physique, dispositif malveillant — si bien que toute la classe est considérée comme un problème matériel.
La désynchronisation des SMI élimine le prérequis : un cœur extérieur, sans accès physique ni matériel requis, peut désormais s'exécuter pendant que le SMM s'exécute — et soudain, les CVE dormantes deviennent exploitables depuis un logiciel.
Dans ce projet, nous avons seulement montré que la fenêtre s'ouvre ; mais cette fenêtre était la raison même pour laquelle ces bogues étaient considérés comme sûrs.
Le vmovdqu par défaut à 0xfcc68860 dans la preuve de concept est un point lent sur cette machine — un Zen 3 Ryzen 7 5800H — et probablement nulle part ailleurs. Pour briser le rendez-vous sur votre machine, vous devrez réajuster l'instruction longue afin que le blocage dure plus longtemps que votre délai SMM. Voici quelques conseils pour y parvenir :
-r xmm → ymm → zmm jusqu'à ce que le blocage dépasse le délai du rendez-vous.make # builds smiiiiiiiiiiiiiiii
Les valeurs par défaut sont réglées pour une seule machine. Sur tout autre chose qu'un Zen 3 Ryzen 7 5800H, attendez-vous à aucune divergence jusqu'à ce que vous réajustiez l'instruction longue — voir Portage vers votre plateforme.
Exécutez l'outil pour déclencher en boucle l'instruction très-très-longue tout en surveillant le compteur SMI de chaque cœur pour détecter une divergence :
sudo ./smiiiiiiiiiiiiiiii # default: -r xmm at 0xfcc68860
Options :
smiiiiiiiiiiiiiiii est un effort de recherche de Christopher Domas (@xoreaxeaxeax).
| Flag | Default | Description |
|---|
-r xmm|ymm|zmm | xmm | Largeur du registre vectoriel pour la lecture MMIO chronométrée (16/32/64 octets). Si aucun écart de compteur SMI n'est observé, l'outil conseille de passer à la taille suivante. |
-a <phys-addr> | 0xfcc68860 | Adresse physique cible pour la boucle de timing MMIO (hex 0x... ou décimale). |
-h, --help | — | Affiche l'aide et quitte. |