
Análise de causa-raiz da CVE-2026-54107: um use-after-free no Windows win32kfull.sys com depuração de condição de corrida, análise estática, insights de triagem do MSRC e pesquisa prática de exploração de kernel.
ValidateHwnd Não É uma Barreira: Causa-Raiz da CVE-2026-54107Um use-after-free no gerenciamento do ciclo de vida de janelas em
win32kfull.sys— como o encontrei, como me convenci de que era real e como o processo do MSRC realmente se parecia do lado do pesquisador.
Eram quase 2 da manhã quando a VM alvo parou de responder ao heartbeat do depurador e caiu em um break exatamente na instrução que eu passei semanas argumentando que era alcançável. Não uma asserção, não uma parada de pool corrompido — uma simples violação de acesso no caminho de despacho de mensagens, desreferenciando um objeto que outra thread já havia destruído.
Esse break se tornou a CVE-2026-54107, caso 11xxxxx do MSRC, corrigida na Atualização de Segurança de julho de 2026 em 27 produtos Windows.
Este post é a metade não embargada da história: causa raiz, por que a classe de bug é o que é e o raciocínio que me levou até lá. Detalhes de exploração ficam de fora.
Win32k é a metade em modo kernel do subsistema gráfico do Windows. É antigo, é enorme e — criticamente — é alcançável a partir de contextos que deveriam ser não confiáveis. Essa última propriedade é a razão pela qual continua sendo um alvo permanente de pesquisa apesar de vinte anos de endurecimento, filtragem e restrição de chamadas de sistema.
Dentro do win32k, o objeto tagWND (PWND) é excepcionalmente interessante porque seu tempo de vida é gerenciado por mais de um mecanismo ao mesmo tempo. Uma janela é:
ValidateHwnd,Qualquer objeto com vários caminhos de referência independentes e um caminho de destruição compartilhado vale a pena ser lido com calma. Isso não é uma afirmação de vulnerabilidade — é uma heurística para onde gastar tempo.
O que me fez parar nesse componente foi a superfície de importação. O win32kfull.sys importa três primitivas distintas de referência de objeto do 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(/* ... */);
Três formas de entrada, um caminho de saída `ObfDereferenceObject`.
Isso não significa que o código esteja errado. Significa que o **invariante é distribuído** — nenhuma função individual é dona de *"este objeto está vivo agora"*, portanto a corretude depende de cada chamador concordar sobre qual referência ele detém e por quanto tempo ela é válida. Invariantes distribuídos são onde as condições de corrida moram, porque uma corrida nunca é um bug de lógica que você pode ver em uma única função. É um bug em uma suposição mantida entre duas.
Então a pergunta que comecei a fazer a toda função que tocava em um `PWND` não era *"este código está correto?"* mas sim:
> **Se exatamente este corpo de função for executado em duas threads com algumas instruções de diferença, qual delas está errada?**
## 3. Causa raiz
O defeito é uma **lacuna de tempo de verificação / tempo de uso entre a liberação da referência e a destruição do objeto** no caminho de destruição da janela, sem sincronização adequada contra um consumidor concorrente que valida handles.
Reduzido à 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 */
}
}
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.