
Ein sehr sehr sehr sehr sehr sehr sehr langer Interrupt
Ausnutzung des System Management Mode mit einem sehr sehr sehr sehr sehr sehr sehr langen Interrupt.
Es stellt sich heraus, dass man SMM — die sichere, hochprivilegierte Ausführungsumgebung, die unsichtbar im Hintergrund jeder x86-CPU läuft — mit nichts weiter als einer obszön lange laufenden Maschineninstruktion brechen kann.
SMM erfordert, dass sich alle Kerne gleichzeitig entweder in SMM oder außerhalb von SMM befinden. Sein Sicherheitsmodell funktioniert ohne dies nicht — wenn ein Thread in SMM eintritt, zwingt er alle anderen ebenfalls zum Eintritt.
Um dies zu brechen, brauchen wir nur jemanden, der zu beschäftigt ist, um zu bemerken, dass er sich SMM anschließen soll.
Es funktioniert ungefähr so:
core 0 - starte eine lange Instruktion
|
|
|
core 1 - lade core 0 zu smm ein
|
|
|
core 1 - trete in smm ein
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - warte auf core 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - gib auf
core 1 - mache geheime smm-Dinge
core 1 - beende smm
|
|
|
core 0 - trete in smm ein
An diesem Punkt ist core 1 außerhalb von SMM, während core 0 darin ist, sodass core 1 core 0 angreifen kann. Hier ist der Haken: Damit dies funktioniert, brauchen wir eine sehr, sehr, seeeehr lange Instruktion — länger, als jede Instruktion jemals dauern sollte. Die meisten Maschineninstruktionen auf einer modernen CPU sind schnell: add dauert 1 Zyklus. Um core 1 dazu zu bringen, auf core 0 aufzugeben, brauchen wir eine Instruktion auf core 0, die etwa 4.000.000.000 Zyklen dauert — über 1 Sekunde Echtzeit.
x86-Firmware führt den folgenden Code aus, wenn ein CPU-Kern in SMM eintritt:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
Der Code wartet darauf, dass alle Kerne in SMM eintreten, oder bis zu 1 Sekunde, je nachdem, was zuerst eintritt. Um einen Kern dazu zu bringen, SMM-Code auszuführen, während ein anderer Kern außerhalb von SMM weiterläuft, muss dieser äußere Kern für die gesamte Sekunde unterbrechungsfrei bleiben — ein SMI wird an einer Instruktionsgrenze angenommen, sodass jede Lücke zwischen zwei Instruktionen es dem ausstehenden SMI ermöglicht, den Kern in SMM zu ziehen. Die Verzögerung muss daher eine einzelne Instruktion sein: eine einzige nicht unterbrechbare Operation, die das Ein-Sekunden-Rendezvous überdauert.
Es gibt viele Wege, um zur verbotenen Ein-Sekunden-Instruktion zu gelangen, und der genaue Ansatz variiert von Plattform zu Plattform. Aber grob gesagt: Finde eine Hochlatenz-MMIO-Adresse und überzeuge dann die CPU, so langsam wie möglich von ihr zu lesen — missbrauche eine undokumentierte Region, die Lesezugriffe im Schneckentempo beantwortet, verwende den breitesten Load, den die ISA dir bietet, um so viele Bytes wie möglich in einer einzigen Instruktion darüber zu bewegen, und lass die anderen Kerne um denselben Bus konkurrieren, um es weiter zu verlangsamen. Ein Lesezugriff, eine Instruktion, und die CPU bleibt für den Großteil einer Sekunde daran hängen.
Das bereitgestellte Proof-of-Concept ist auf einen Zen 3 Ryzen 7 5800H abgestimmt, wo ein breiter xmm-Load von langsamem MMIO bei 0xfcc68860 lange genug anhält, um das All-Kerne-Rendezvous zu brechen:
mov $0xfcc68860, %rsi ; die Ziel-MMIO-Adresse
vmovdqu (%rsi), %xmm0 ; der sehr, sehr lange Load
Das PoC nutzt dies aus, indem es zwei Kerne gegeneinander antreten lässt. Ein Kern wird außerhalb von SMM gehalten durch die lange Instruktion — eine enge Schleife auf dem sehr langsamen Load:
/* der Opferkern: dreht sich auf dem ~1-Sekunden-Load, zu beschäftigt, um den SMI zu beantworten */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
Währenddessen aktiviert ein anderer Kern die Pro-Kern-SMI-Zähler:
#define MSR_PERF_CTL0 0xc0010200 /* AMD-Kern-Perf-Event-Select-MSR */
#define MSR_PERF_CTR0 0xc0010201 /* der gepaarte 48-Bit-Zähler */
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); /* Zähler auf null setzen */
}
Dann feuert einen Sturm von SMIs ab:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* Port 0xb2 anstoßen -> #SMI */
Und liest die Bilanz jedes Kerns zurück:
/* ...Sturm abfeuern, dann die Bilanz jedes Kerns zurücklesen... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! ein Kern lief außerhalb von SMM");
Wenn die Zähler voneinander abweichen, bedeutet das, dass ein Kern außerhalb von SMM weiterlief, während die anderen hineingezogen wurden — er verpasste die SMIs, die die übrigen abarbeiteten.
Um dies besser zu veranschaulichen, können wir das Proof-of-Concept hinter einer unnötig auffälligen und völlig nutzlosen GUI ausführen, die SMI-Zähler auf jedem Kern verfolgt, um zu beobachten, wie sie in perfektem Gleichschritt ausgeführt werden, bis ein stark verzögerter Kern ihre erforderliche Synchronisation bricht:

SMMs einzige Garantie — dass nichts anderes läuft, während es läuft — fällt unter einer einzigen absurden langen Instruktion auseinander.
SMMs Sicherheit beruht auf einer einfachen Annahme: Während es läuft, läuft nichts anderes.
Es gibt 100+ SMM TOCTOU CVEs da draußen: ein SMM Handler prüft einen Wert im gemeinsamen Speicher, und verwendet ihn dann. Alles, was du für die Ausnutzung brauchst, ist, diesen Wert zwischen Prüfung und Verwendung neu zu schreiben, und du bist in SMM. Aber diese Probleme liegen schlummernd und weitgehend ungepatcht in freier Wildbahn, wegen einer Annahme: Ausnutzung erfordert etwas, das den gemeinsamen Speicher während der SMM-Ausführung modifiziert, und wegen des SMM-Rendezvous befinden sich keine CPU-Kerne außerhalb von SMM, um einen Angriff zu starten. Der einzige Weg hinein war ein DMA-fähiges Peripheriegerät, das hinter dem Rücken der CPU schreibt — physischer Zugriff, ein bösartiges Gerät —, sodass die gesamte Klasse als Hardwareproblem abgeschrieben wird.
SMI-Desynchronisierung entfernt die Voraussetzung, die die Plattform sicher hielt: Ein äußerer Kern, ohne physischen Zugriff oder Hardware, kann jetzt während der SMM-Ausführung laufen — und die schlummernden CVEs werden aus Software heraus ausnutzbar.
Es gibt wahrscheinlich keine, was dieses Problem etwas interessanter macht als traditionelle SMM-Probleme. Behalte das Timeout bei, und das Rendezvous ist leicht zu brechen. Entferne das Timeout, und ein legitimerweise festhängender Kern hängt die Plattform beim ersten SMI auf. Erhöhe das Timeout, und du tötest die Leistung auf Plattformen mit vielen Kernen, die gezwungen sind, alle Kerne bei jedem SMM-Eintritt zu beruhigen. Es ist nicht klar, was der beste Weg nach vorne ist, oder ob es überhaupt einen Weg nach vorne gibt.
Bis dahin ist die empfohlene Abhilfe, keine langen Instruktionen auszuführen.
Der Standard-vmovdqu bei 0xfcc68860 im Proof-of-Concept ist eine langsame Stelle auf dieser Maschine — einem Zen 3 Ryzen 7 5800H — und wahrscheinlich nirgendwo sonst. Um das Rendezvous auf deiner Maschine zu brechen, musst du die lange Instruktion neu abstimmen, sodass der Stillstand dein SMM-Timeout überdauert. Einige Tipps, wie du das machst:
-r xmm → ymm → zmm, bis der Stillstand das Rendezvous-Timeout überschreitet.make # erstellt smiiiiiiiiiiiiiiii
Die Standardwerte sind auf eine Maschine abgestimmt. Auf allem außer einem Zen 3 Ryzen 7 5800H erwarte keine Divergenz, bis du die lange Instruktion neu abstimmst — siehe Portierung auf deine Plattform.
Führe das Tool aus, um wiederholt die sehr-sehr-lange Instruktion abzufeuern, während du den SMI-Zähler jedes Kerns auf eine Divergenz beobachtest:
sudo ./smiiiiiiiiiiiiiiii # Standard: -r xmm bei 0xfcc68860
Flags:
| Flag | Standard | Beschreibung |
|---|---|---|
-r xmm|ymm|zmm | xmm | Vektorregisterbreite für den zeitgesteuerten MMIO-Lesezugriff (16/32/64 Bytes). Wenn keine SMI-Zähldifferenz beobachtet wird, empfiehlt das Tool, auf die nächste Größe zu erhöhen. |
-a <phys-addr> | 0xfcc68860 | Ziel-Physische-Adresse für die MMIO-Timing-Schleife (hex 0x... oder dezimal). |
-h, --help | — | Verwendung ausgeben und beenden. |
smiiiiiiiiiiiiiiii ist ein Forschungsprojekt von Christopher Domas (@xoreaxeaxeax).