
Analisi della causa principale di CVE-2026-54107: un use-after-free in Windows win32kfull.sys con debug di race condition, analisi statica, approfondimenti sul triage MSRC e ricerca pratica sullo sfruttamento del kernel.
ValidateHwnd Non È un Cancello: Analisi delle Cause di CVE-2026-54107Un use-after-free nella gestione del ciclo di vita delle finestre in
win32kfull.sys— come l'ho trovato, come mi sono convinto che fosse reale, e come appariva davvero il processo MSRC dal lato del ricercatore.
Erano quasi le 2 di notte quando la VM di test ha smesso di rispondere al heartbeat del debugger ed è caduta in un breakpoint esattamente sull'istruzione che per settimane avevo sostenuto essere raggiungibile. Non un'asserzione, non uno stop per pool corrotto — una semplice violazione di accesso sul percorso di dispatch dei messaggi, nel dereferenziare un oggetto che un altro thread aveva già smantellato.
Quel breakpoint è diventato CVE-2026-54107, caso MSRC 11xxxxx, corretto nell'Aggiornamento di sicurezza di luglio 2026 in 27 prodotti Windows.
Questo post è la metà non soggetta a embargo della storia: causa principale, perché la classe di bug è quella che è, e il ragionamento che mi ha portato fin qui. I dettagli di sfruttamento restano fuori.
Win32k è la metà in modalità kernel del sottosistema grafico di Windows. È vecchio, è enorme e — cosa cruciale — è raggiungibile da contesti che dovrebbero essere non fidati. Quest'ultima proprietà è il motivo per cui rimane un bersaglio di ricerca permanente nonostante vent'anni di lavoro su hardening, filtri e restrizioni sulle syscall.
All'interno di win32k, l'oggetto tagWND (PWND) è insolitamente interessante perché il suo ciclo di vita è gestito da più di un meccanismo contemporaneamente. Una finestra è:
ValidateHwnd,Qualsiasi oggetto con diversi percorsi di riferimento indipendenti e un unico percorso di distruzione condiviso merita di essere letto con calma. Non è un'affermazione di vulnerabilità — è un'euristica su dove spendere tempo.
Ciò che mi ha fatto soffermare su questo componente è stata la superficie di importazione. win32kfull.sys tira dentro tre primitive distinte di riferimento a oggetti da ntoskrnl:```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(/* ... */);
Three vie in ingresso, un solo percorso di uscita `ObfDereferenceObject`.
Ciò non significa che il codice sia sbagliato. Significa che l'**invariante è distribuita** — nessuna singola funzione possiede *"questo oggetto è vivo in questo momento,"* quindi la correttezza dipende dal fatto che ogni chiamante concordi su quale riferimento detiene e per quanto tempo sia valido. Le invarianti distribuite sono il luogo in cui vivono le race condition, perché una race non è mai un bug logico che puoi vedere in una singola funzione. È un bug in un'ipotesi condivisa tra due.
Quindi la domanda che ho iniziato a porre a ogni funzione che toccava un `PWND` non era *"questo codice è corretto?"* ma:
> **Se questo esatto corpo di funzione viene eseguito su due thread a pochi istruzioni di distanza, quale dei due è sbagliato?**
## 3. Causa principale
Il difetto è un **intervallo time-of-check / time-of-use tra il rilascio del riferimento e la distruzione dell'oggetto** nel percorso di distruzione della finestra, senza una sincronizzazione adeguata rispetto a un consumer concorrente che convalida handle.
Ridotto alla sua forma:```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 */
}
}
(Mostra il menu completo con tutti gli argomenti, le loro descrizioni e i valori predefiniti. Questo parametro non richiede un valore, basta inserirlo come '--help')```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 */
...
}
Due cose devono essere vere perché questo conti, ed entrambe lo erano:
**(a) La finestra è reale.** `ValidateHwnd` è il gate che dovrebbe rendere sicuro l'accesso basato sugli handle. Se la validazione può avere successo contro un oggetto la cui distruzione è già iniziata, il gate non è un gate — è un suggerimento.
**(b) La memoria liberata è influenzabile dall'attaccante.** I campi letti subito dopo la validazione includono `fnid`, che guida il dispatch dei messaggi. Una decisione di dispatch presa da memoria recuperata è la differenza tra *"crash inaffidabile"* e *"violazione del confine di sicurezza"*. Questa distinzione è l'intera ragione per cui questa è CWE-362 con impatto EoP e non un bug di stabilità.
> La **corruzione** osservata è una use-after-free; la **causa** è CWE-362, esecuzione concorrente che usa una risorsa condivisa con sincronizzazione impropria. Sono due affermazioni diverse e a MSRC interessa la seconda. **Riporta la causa, non solo il sintomo.**
### Perché le race condition in win32k sono strutturalmente più difficili di quanto sembrano
Se hai fatto race in altri sottosistemi, win32k ti frustrerà, perché l'architettura ti combatte in tre modi specifici.