
Root-Cause-Analyse von CVE-2026-54107: ein Use-after-Free in Windows win32kfull.sys mit Race-Condition-Debugging, statischer Analyse, MSRC-Triage-Erkenntnissen und praktischer Forschung zur Kernel-Exploitation.
ValidateHwnd kein Tor ist: Root-Cause-Analyse von CVE-2026-54107Ein Use-after-Free in der Fenster-Lebenszyklusverwaltung von
win32kfull.sys– wie ich ihn gefunden habe, wie ich mich davon überzeugt habe, dass er echt ist, und wie der MSRC-Prozess aus Sicht des Forschers tatsächlich aussieht.
Es war kurz vor 2 Uhr morgens, als die Ziel-VM nicht mehr auf den Heartbeat des Debuggers reagierte und exakt an der Anweisung in einen Break fiel, von der ich wochenlang behauptet hatte, dass sie erreichbar sei. Kein Assertion, kein Corrupted-Pool-Stopp – eine einfache Access Violation auf dem Nachrichten-Dispatch-Pfad, die ein Objekt dereferenzierte, das ein anderer Thread bereits abgerissen hatte.
Dieser Break wurde zu CVE-2026-54107, MSRC-Fall 11xxxxx, gepatcht im Sicherheitsupdate vom Juli 2026 für 27 Windows-Produkte.
Dieser Beitrag ist die nicht unter Embargo stehende Hälfte der Geschichte: Root Cause, warum die Bug-Klasse genau diese ist, und die Argumentation, die mich dorthin geführt hat. Exploitation-Details bleiben außen vor.
Win32k ist die Kernel-Mode-Hälfte des Windows-Grafiksubsystems. Es ist alt, es ist riesig – und, entscheidend: Es ist aus Kontexten erreichbar, die als nicht vertrauenswürdig gelten. Diese letzte Eigenschaft ist der Grund, warum es trotz zwanzig Jahren Härtung, Filterung und Syscall-Einschränkungen ein permanentes Forschungsziel bleibt.
Innerhalb von win32k ist das Objekt tagWND (PWND) ungewöhnlich interessant, weil seine Lebensdauer von mehr als einem Mechanismus gleichzeitig verwaltet wird. Ein Fenster ist:
ValidateHwnd,Jedes Objekt mit mehreren unabhängigen Referenzpfaden und einem gemeinsamen Zerstörungspfad ist es wert, langsam gelesen zu werden. Das ist kein Vulnerability-Anspruch – es ist eine Heuristik dafür, wo man Zeit investieren sollte.
Was mich bei dieser Komponente verweilen ließ, war die Import-Oberfläche. win32kfull.sys zieht drei verschiedene Objektreferenz-Primitive aus ntoskrnl herein:```c
NTSTATUS ObReferenceObjectByPointer(
void *Object,
uint32_t DesiredAccess,
POBJECT_TYPE ObjectType,
char AccessMode);
NTSTATUS ObReferenceObjectByHandle( HANDLE Handle, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode, void **Object, OBJECT_HANDLE_INFORMATION *HandleInformation);
NTSTATUS ObReferenceObjectByName(/* ... */);
Drei Wege hinein, ein `ObfDereferenceObject`-Pfad hinaus.
Das bedeutet nicht, dass der Code falsch ist. Es bedeutet, dass die **Invariante verteilt ist** — keine einzelne Funktion besitzt *"dieses Objekt ist jetzt gerade lebendig,"* daher hängt die Korrektheit davon ab, dass jeder Aufrufer sich darüber einig ist, welche Referenz er hält und wie lange sie gültig ist. Verteilte Invarianten sind der Ort, an dem Race Conditions leben, denn eine Race ist niemals ein Logikfehler, den man in einer einzelnen Funktion sehen kann. Es ist ein Fehler in einer Annahme, die über zwei hinweg gehalten wird.
Also war die Frage, die ich jeder Funktion, die ein `PWND` berührte, zu stellen begann, nicht *"ist dieser Code korrekt?"*, sondern:
> **Wenn dieser exakte Funktionsrumpf auf zwei Threads im Abstand weniger Anweisungen ausgeführt wird, welcher von ihnen ist falsch?**
## 3. Grundursache
Der Defekt ist eine **Time-of-Check/Time-of-Use-Lücke zwischen Referenzfreigabe und Objektabbau** im Fensterzerstörungspfad, ohne ausreichende Synchronisation mit einem gleichzeitigen Konsumenten, der Handles validiert.
Auf seine Grundform reduziert:```c
/* Thread A — teardown */
NtUserDestroyWindow(HWND hwnd)
{
PWND pWnd = ValidateHwnd(hwnd);
if (pWnd) {
ObfDereferenceObject(pWnd); /* reference released */
/* <-- race window: object may become reclaimable here */
FreeWindowObject(pWnd); /* teardown proceeds on a pointer
no longer guaranteed live */
}
}
I apologize, but I notice the input chunk appears to be empty — no Markdown content was provided after "INPUT:".
Could you please resend chunk 5 of 13 with the actual English content you'd like translated into German? Once the source text is provided, I'll translate it immediately while preserving all Markdown structure exactly as specified.```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */
if (pWnd->fnid == FNID_BUTTON) /* use-after-free */
...
}
Two things have to be true for this to matter, and both were:
**(a) The window is real.** `ValidateHwnd` is the gate that's supposed to make handle-based access safe. If validation can succeed against an object whose teardown has already begun, the gate isn't a gate — it's a suggestion.
**(b) The freed memory is attacker-influenceable.** The fields read immediately after validation include `fnid`, which drives message dispatch. A dispatch decision made from reclaimed memory is the difference between *"unreliable crash"* and *"security boundary violation."* That distinction is the entire reason this is CWE-362 with EoP impact and not a stability bug.
> The observed **corruption** is a use-after-free; the **cause** is CWE-362, concurrent execution using a shared resource with improper synchronization. Those are two different statements and MSRC cares about the second one. **Report the cause, not just the symptom.**
### Why win32k races are structurally harder than they look
If you've raced bugs in other subsystems, win32k will frustrate you, because the architecture fights you in three specific ways.