Author: Felipe XavierIntrodução
Este artigo analisa o CVE-2026-32217, uma vulnerabilidade que expõe endereços do kernel por meio do Windows Event Log. O caminho afetado envolvia registros WHEA disponíveis no canal Microsoft-Windows-Kernel-WHEA/Operational, nos quais endereços virtuais do kernel podiam ser obtidos por um usuário local com baixos privilégios.
Depois de algum tempo analisando vulnerabilidades relacionadas ao Windows Event Log, assisti a uma palestra de Yarden Shafir sobre as mudanças recentes nas proteções contra vazamentos de endereços do kernel. A apresentação forneceu o estímulo que faltava para que eu voltasse a investigar essa superfície de ataque com mais atenção.
Um dos detalhes mais interessantes da pesquisa era a origem de alguns endereços do kernel, esses endereços virtuais podiam ser encontrados diretamente nos logs de eventos do Windows. Em outras palavras, informações suficientes para burlar o KASRL estavam disponíveis a poucos cliques no Event Viewer.
Minha primeira reação foi verificar se o mesmo comportamento ainda podia ser reproduzido na versão do Windows instalada em minha máquina. Comecei enumerando os canais de eventos e automatizando a leitura das mensagens renderizadas, procurando valores compatíveis com endereços canônicos do kernel — em particular, ponteiros localizados no intervalo virtual normalmente associado às imagens carregadas em modo kernel:
O resultado inicial foi negativo. Os endereços observados na pesquisa original não apareciam mais no texto apresentado pelo Event Viewer. No entanto, a ausência de um valor na representação renderizada de um evento não demonstra que ele tenha sido removido do registro original.
O Event Viewer não exibe necessariamente o conteúdo bruto armazenado pelo subsistema de eventos. O que o usuário vê é uma representação produzida por sucessivas camadas de serialização, consulta, renderização e formatação. Campos internos, estruturas binárias e dados que não participam da mensagem final podem continuar presentes no evento, mesmo quando não são exibidos pela interface gráfica.
Isso levou a uma hipótese mais específica: talvez a correção tivesse removido os endereços apenas da camada de apresentação, sem necessariamente eliminá-los na origem ou no formato binário transportado pelo serviço. A questão, portanto, deixou de ser simplesmente “o ponteiro ainda aparece no Event Viewer?” e passou a ser:
“Em qual estágio do pipeline o endereço foi removido: durante a geração do evento, na serialização, no armazenamento, no transporte RPC ou apenas durante a renderização da mensagem?”
Para responder a essa pergunta, seria necessário abandonar a interface gráfica e observar o Windows Event Log em uma camada inferior. Foi nesse ponto que cheguei ao serviço responsável pelo processamento das consultas e à interface RPC MS-EVEN6 implementada por wevtsvc.dll.
Este artigo descreve como respondi a essa pergunta de duas maneiras distintas. A primeira maneira foi através da consulta direta da interface RPC MS-EVEN6 implementada pelo serviço Windows Event Log e a análise dos registros no formato BinXml devolvidos pelo servidor RPC.
A segunda maneira, foi através de consultas realizadas na API WinEvt, usando as implementações das APIs EvtQuery, EvtNext e EvtRender.
Mapeando os componentes RPC do Event Log
A análise do processo em questão confirmou que o serviço Windows Event Log estava sendo executado por uma instância dedicada de svchost.exe:
Ao inspecionar os handles mantidos por esse processo, foi possível identificar um objeto do tipo ALPC Port:
Esse objeto corresponde ao endpoint utilizado pelo RPC local ncalrpc. Em outras palavras, o nome eventlog observado no namespace \RPC Control é o endpoint empregado pela RPC runtime para encaminhar chamadas locais até o serviço Windows Event Log.
A presença desse handle no processo confirma a relação entre o serviço e o endpoint ALPC, mas ainda não identifica, isoladamente, quais interfaces RPC estão registradas sobre ele. Essa correlação exige enumerar as interfaces expostas pelo serviço e associá-las ao endpoint encontrado.
Em seguida, examinei o security descriptor do objeto. A DACL continha uma ACE aplicável ao grupo Everyone, concedendo direitos de acesso ao endpoint. Isso é compatível com a arquitetura do Windows Event Log: processos executados por usuários comuns precisam ser capazes de alcançar o serviço para consultar os canais aos quais possuem acesso.
A permissão DELETE exibido para o objeto ALPC é padrão sobre o objeto do kernel; ele não representa autorização para apagar registros de eventos. Além disso, conseguir abrir o endpoint não implica autorização para consultar qualquer canal. O servidor ainda executa o access check específico da operação e do canal.
Através da utilização da ferramenta RpcView, foi possível identificar uma característica importante: o serviço Windows Event Log não expunha uma única interface RPC, mas duas interfaces distintas, cada uma identificada por seu próprio par UUID/versão e contendo uma quantidade específica de procedures.
Breve descrição sobre RPCs, Procedures e Interfaces
No contexto de RPC, uma procedure é uma operação exposta por uma interface. Ela é identificada no protocolo pelo seu operation number (opnum), dentro de uma interface definida por um UUID e uma versão específica. O cliente não precisa selecionar a operação por seu nome: o RPC runtime serializa os argumentos conforme os descritores NDR e envia uma requisição contendo a identificação da interface e o opnum. No servidor, esse número é utilizado para localizar a entrada correspondente na dispatch table e encaminhar a chamada ao stub ou à rotina responsável por sua implementação.
Já uma interface é o contrato que agrupa um conjunto de procedures e define como elas podem ser chamadas. Cada interface é identificada de forma única pela combinação de um UUID e uma versão, além de conter as assinaturas das procedures, seus respectivos opnums e as descrições NDR necessárias para serializar parâmetros e valores de retorno. A interface não deve ser confundida com o endpoint. O endpoint determina como alcançar o servidor, enquanto o UUID e a versão determinam qual contrato será utilizado depois que a conexão for estabelecida. Dessa forma, um mesmo processo pode hospedar diversas interfaces RPC, e várias delas podem estar disponíveis por meio do mesmo endpoint de transporte.
Essa distinção é relevante porque serviço, endpoint, procedure e interface RPC não são conceitos equivalentes. Cada interface possui sua própria dispatch table, seus descritores NDR e seu conjunto de operações.
No protocolo RPC, uma operação não é selecionada pelo nome textual da função, mas por um operation number, ou opnum. Os nomes apresentados por ferramentas como RpcView e NtObjectManager são informações auxiliares obtidas por símbolos, definições IDL conhecidas ou heurísticas de descompilação. Quando nenhum nome pode ser recuperado, as procedures são representadas por identificadores falsos como Proc0, Proc1 e Proc5.
Portanto, uma chamada RPC é essencialmente determinada pela seguinte combinação:
Interface UUID + versão + opnum
O endpoint define como alcançar o servidor; o UUID e a versão selecionam a interface; e o opnum identifica a procedure que será despachada dentro dela.
RPC possui vários outros detalhes importantes — bindings, transports, context handles, stubs MIDL, serialização NDR e mecanismos de autenticação, entre outros. Uma explicação completa desses componentes desviaria o foco desta análise, portanto deixarei ao final do artigo algumas referências para quem quiser se aprofundar na arquitetura RPC do Windows.
A imagem a seguir demonstra o output da ferramenta RpcView. Perceba que wevtsvc.dll registra duas interfaces RPC distintas, devidamente identificadas pelos UUIDs 82273fdc-e32a-18c3-3f78-827929dc23ea e f6beaff7-1e19-4fbb-9f8f-b89e2018337c.
A primeira declara a versão 0.0 e expõe 27 procedures; a segunda declara a versão 1.0 e expõe 29:
Conforme expliquei anteriormente, não se tratam de duas versões de um mesmo conjunto de funções, mas de duas interfaces independentes, cada uma com seu próprio UUID, versão e dispatch table.
Na serviço eventlog analisado, existiam endpoints para três das protocol sequences mais comuns do RPC no Windows, ncalrpc, ncacn_np e ncacn_ip_tcp:
Cada uma representa um transporte diferente:
- ncalrpc: comunicação RPC local, implementada sobre ALPC;
- ncacn_np: comunicação por Named Pipes, normalmente transportada por SMB;
- ncacn_ip_tcp: comunicação RPC diretamente sobre TCP/IP.
O endpoint ncacn_ip_tcp estava associado à porta TCP/49667. Esse valor, entretanto, não deve ser interpretado como uma porta fixa do Windows Event Log. Trata-se de uma porta dinâmica atribuída àquela instância do serviço e que pode mudar após uma reinicialização ou em outra versão do sistema.
Em princípio, um cliente compatível poderia alcançar o serviço por qualquer um desses transportes, desde que satisfizesse os requisitos de binding, autenticação, autorização e acessibilidade de rede. Para esta pesquisa, escolhi ncalrpc, pois o objetivo inicial era analisar exclusivamente a comunicação local com o serviço. Essa escolha também eliminava variáveis adicionais introduzidas por SMB, pelo Endpoint Mapper, pela autenticação remota e pela configuração do firewall.
O RpcView também oferece a possibilidade de descompilar cada interface para uma representação aproximada em IDL. Esse recurso é particularmente útil porque os descritores NDR permitem recuperar características como:
- Direção dos parâmetros, incluindo os atributos [in], [out] e [in, out];
- Tipos primitivos utilizados na serialização;
- Tipos de ponteiros;
- Estruturas, arrays e tamanhos;
- Context handles;
- Tipo do valor de retorno.
Entretanto, o IDL descompilado não preserva necessariamente as informações semânticas presentes no código-fonte original. Sem símbolos ou uma definição IDL pública, nomes de funções, parâmetros, estruturas e campos podem aparecer como identificadores genéricos.
Desta forma, embora o RpcView reconstrua grande parte do que é necessário para serializar uma chamada válida, ele não explica o significado de cada argumento. A etapa seguinte da engenharia reversa consiste justamente em correlacionar esses tipos com a implementação das funções de dispatch, seus call sites e as estruturas internas manipuladas pelo serviço.
Buscando documentação pública sobre wevtsvc.dll e sobre o serviço Windows Event Log, encontrei duas especificações mantidas pela Microsoft: MS-EVEN — EventLog Remoting Protocol eMS-EVEN6 — EventLog Remoting Protocol Version 6.0.
A documentação permitiu correlacionar diretamente as duas interfaces observadas no RpcView:
| Protocolo | UUID | Versão | Procedures |
|---|---|---|---|
| MS-EVEN | 82273fdc-e32a-18c3-3f78-827929dc23ea | 0.0 | 27 |
| MS-EVEN6 | f6beaff7-1e19-4fbb-9f8f-b89e2018337c | 1.0 | 29 |
Portanto, já não era necessário inferir que a interface 0.0 poderia corresponder ao protocolo antigo. A própria especificação MS-EVEN define o UUID 82273fdc-e32a-18c3-3f78-827929dc23ea, versão 0.0, utilizando o endpoint conhecido \PIPE\eventlog. Da mesma forma, a especificação MS-EVEN6 define o UUID f6beaff7-1e19-4fbb-9f8f-b89e2018337c, versão 1.0, e o endpoint RPC EventLog.
A Microsoft descreve MS-EVEN6 como a versão 6.0 do protocolo de acesso remoto ao Windows Event Log, introduzida originalmente no Windows Vista. Embora ambas as interfaces continuem implementadas em wevtsvc.dll, MS-EVEN6 substitui o protocolo anterior e oferece um modelo de consulta mais próximo da infraestrutura moderna, incluindo filtros, subscriptions, metadados de publishers e formatação de mensagens.
A documentação também contém algo ainda mais útil para esta análise: a definição dos métodos RPC, seus números de operação e suas assinaturas IDL. Por exemplo, a seção referente a EvtRpcRegisterLogQuery associa explicitamente esse método ao opnum 5.
Essa informação estabelece a correlação direta com a procedure apresentada pelas ferramentas de descompilação como Proc5 (identificada pela ferramenta RpcView).
A especificação fornece não apenas o nome, mas também o contrato de serialização da operação:
É importante observar que isso não corresponde à implementação interna da função. A Microsoft publica a definição protocolar, os tipos transmitidos, as regras de processamento e o comportamento esperado do servidor, mas não o código-fonte de wevtsvc.dll.
Ainda assim, essas informações eliminam grande parte da ambiguidade produzida pelo IDL descompilado: Proc5, p0, p1 e os demais nomes genéricos podem ser correlacionados com a função EvtRpcRegisterLogQuery e seus respectivos parâmetros de saída.
Nesse ponto, a interface já estava identificada, o endpoint era conhecido e a assinatura do procedimento estava documentada. A próxima questão passou a ser puramente operacional:
Como construir um cliente RPC capaz de realizar o binding com essa interface, serializar os parâmetros de acordo com o NDR e invocar diretamente o opnum 5?
Depois de identificar a interface MS-EVEN6, o passo seguinte foi determinar qual de suas procedures permitiria reproduzir o fluxo mínimo de consulta a um determinado canal.
Ao invés de analisar as 29 operações de forma indiscriminada, eu parti do modelo de execução documentado para o protocolo. Nesse fluxo, EvtRpcRegisterLogQuery, correspondente ao opnum 5, funciona como o ponto de entrada para a criação de uma consulta. O cliente fornece o caminho do canal, a expressão de consulta e as flags aplicáveis, enquanto o servidor inicializa o estado necessário e devolve os context handles que representam aquela operação.
É importante observar que EvtRpcRegisterLogQuery não retorna diretamente o conteúdo dos eventos. Sua função é registrar a consulta e estabelecer o contexto que será consumido por chamadas subsequentes, especialmente EvtRpcQueryNext, responsável por recuperar os registros em lotes.
A escolha dessa procedure, portanto, não ocorreu porque ela própria aparentava conter o disclosure, mas porque ela inicia o caminho protocolar mais direto até a representação dos eventos fornecida pelo serviço, antes mesmo de qualquer processamento ou apresentação realizado pelas camadas públicas da API WinEvt.
Para construir a PoC em PowerShell, utilizei o módulo NtObjectManager. O módulo analisa os metadados MIDL/NDR incorporados ao binário do servidor, reconstrói as definições dos procedures e gera dinamicamente um cliente RPC em C#. O código gerado é então compilado em memória e exposto como um objeto .NET que pode ser utilizado diretamente pelo PowerShell.
Isso evita três etapas que, de outra forma, precisariam ser realizadas manualmente:
- Reconstruir uma definição IDL compatível;
- Processar essa IDL com o compilador MIDL;
- Implementar o cliente e o marshalling NDR dos parâmetros.
O cliente foi preparado da seguinte forma:
Import-Module NtObjectManager -ErrorAction Stop
$interfaceId = 'f6beaff7-1e19-4fbb-9f8f-b89e2018337c'
$serverPath = "$env:SystemRoot\System32\wevtsvc.dll"
$servers = Get-RpcServer -FullName $serverPath
$eventService = $servers |
Where-Object {
$_.InterfaceId.ToString() -eq $interfaceId -and
$_.InterfaceVersion.ToString() -eq '1.0'
} |
Select-Object -First 1
if (-not $eventService) {
throw 'A interface MS-EVEN6 v1.0 não foi encontrada em wevtsvc.dll.'
}
$client = Get-RpcClient -Server $eventService
Connect-RpcClient `
-Client $client `
-EndpointPath 'eventlog' `
-ProtocolSequence 'ncalrpc'
if (-not $client.Connected) {
throw 'Não foi possível estabelecer o binding RPC.'
}
Write-Host '[+] Binding estabelecido com ncalrpc:[eventlog]'
Get-RpcServer analisa wevtsvc.dll e procura suas estruturas RPC, incluindo a identificação da interface, a dispatch table e os descritores de procedimentos NDR. Depois de selecionar a interface MS-EVEN6, Get-RpcClient produz uma implementação (cliente) compatível com as assinaturas recuperadas.
Por fim, Connect-RpcClient estabelece a associação RPC com o endpoint ncalrpc:[eventlog]. A partir desse momento, o objeto $client representa um cliente conectado à interface MS-EVEN6 v1.0.
As chamadas subsequentes não passam pela API WinEvt exposta por wevtapi.dll. Em vez disso, são serializadas pelo cliente e enviadas diretamente à procedure correspondente da interface RPC.
Essa distinção altera a camada de observação, mas não remove os controles de segurança do serviço. O Windows Event Log continua recebendo a identidade do caller e pode aplicar verificações de acesso específicas para cada método, canal ou arquivo consultado. É bom lembrar que, o acesso direto ao RPC contorna a abstração da API pública — não a autorização implementada pelo servidor.
Resolvendo procedures com NtObjectManager
Ao analisar uma interface RPC diretamente de uma DLL do Windows, o NtObjectManager consegue recuperar seu UUID, versão, quantidade de procedures, números de operação, descritores NDR, parâmetros e endereços das funções presentes na dispatch table.
Entretanto, conforme explicado anteriormente, os metadados MIDL/NDR não precisam preservar os nomes semânticos definidos originalmente no código-fonte.
Esses nomes não pertencem ao protocolo nem necessariamente existem em wevtsvc.dll. Eles são gerados pelo NtObjectManager a partir do número de operação de cada procedure.
No caso da interface MS-EVEN6:
Interface UUID: f6beaff7-1e19-4fbb-9f8f-b89e2018337c
Versão: 1.0
Servidor: C:\Windows\System32\wevtsvc.dll
Endpoint local: ncalrpc:[eventlog]
Procedimentos: 29
O objetivo desta etapa era verificar se o endereço de dispatch associado a Proc5 poderia ser resolvido por um símbolo interno publicado no PDB correspondente.
É importante esclarecer que o NtObjectManager continua utilizando os metadados NDR para reconstruir a interface e a assinatura das procedures. O que muda é a origem do campo Name.
A resolução ocorre aproximadamente da seguinte forma:
O NDR permite descobrir como os argumentos são serializados. A dispatch table fornece o endereço da rotina associada ao opnum. Por fim, o DbgHelp consulta o PDB (Program Database) correspondente àquela build e tenta converter o endereço em um símbolo.
Essa funcionalidade é descrita por James Forshaw em Calling Local Windows RPC Servers from .NET. O artigo demonstra que a Get-RpcServer aceita o parâmetro -DbgHelpPath para resolver nomes de procedures por meio dos símbolos públicos.
Instalando o Debugging Tools for Windows
Para acessar corretamente o Microsoft Public Symbol Server, utilize a versão fornecida pelo Debugging Tools for Windows, que também acompanha symsrv.dll e symchk.exe.
O pacote pode ser instalado como componente do Windows SDK. A Microsoft também permite instalar apenas as ferramentas de depuração, desmarcando os demais componentes do SDK.
O instalador está disponível na documentação oficial:
Configurando o Microsoft Public Symbol Server
Para armazenar os PDBs no perfil do usuário:
$symbolCache = Join-Path $env:LOCALAPPDATA "MicrosoftSymbols"
$symbolServer = "https://msdl.microsoft.com/download/symbols"
$symbolPath = "srv*$symbolCache*$symbolServer"
New-Item `
-Path $symbolCache `
-ItemType Directory `
-Force | Out-Null
$env:_NT_SYMBOL_PATH = $symbolPath
Write-Host $env:_NT_SYMBOL_PATH
A saída será semelhante a:
srv*C:\Users\redway\AppData\Local\MicrosoftSymbols*https://msdl.microsoft.com/download/symbols
Essa configuração afeta somente o processo atual do PowerShell. A sintaxe e o uso de _NT_SYMBOL_PATH são documentados pela Microsoft em Configure Symbol Path.
Validando o PDB de wevtsvc.dll
Antes de executar o NtObjectManager, podemos utilizar symchk.exe para localizar e validar o PDB correspondente ao binário instalado.
O script a seguir encontra o diretório x64 do Debugging Tools e executa o SymChk:
$dllPath = "$env:SystemRoot\System32\wevtsvc.dll"
$debuggerDirectories = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\Debuggers\x64",
"$env:ProgramFiles\Windows Kits\10\Debuggers\x64"
)
$debuggerDirectory = $debuggerDirectories |
Where-Object {
$_ -and
(Test-Path (Join-Path $_ "dbghelp.dll")) -and
(Test-Path (Join-Path $_ "symsrv.dll")) -and
(Test-Path (Join-Path $_ "symchk.exe"))
} |
Select-Object -First 1
if (-not $debuggerDirectory) {
throw "Debugging Tools for Windows não foi encontrado."
}
$symChkPath = Join-Path $debuggerDirectory "symchk.exe"
& $symChkPath `
$dllPath `
"/s" `
$env:_NT_SYMBOL_PATH `
"/v"
O SymChk lê o PE Debug Directory e o registro CodeView/RSDS, que contém o nome do PDB, GUID e Age. Não basta encontrar qualquer arquivo chamado wevtsvc.pdb; o identificador e a idade do PDB precisam corresponder aos valores registrados em wevtsvc.dll.
Depois da execução, o arquivo pode ser localizado no cache:
Get-ChildItem `
-Path "$env:LOCALAPPDATA\MicrosoftSymbols" `
-Filter "wevtsvc.pdb" `
-Recurse `
-File `
-ErrorAction SilentlyContinue
Resolvendo os nomes com NtObjectManager
Construí um script capaz de configurar o symbol server, validar o PDB e solicitar ao NtObjectManager que resolva as procedures da interface MS-EVEN6. Quando o PDB público contém o símbolo associado ao endereço da dispatch table, o resultado pode ser semelhante a:
Name : EvtRpc_s_RegisterLogQuery
ProcNum : 5
Os scripts e códigos-fontes serão disponibilizados no github da Redway Security.
A imagem a seguir demonstra uma parte do output do script.
A nomenclatura precisa pode variar entre builds. O símbolo interno também não precisa ser idêntico ao nome utilizado na especificação do protocolo.
Nesse caso, existem três representações distintas:
| Representação | Nome | Origem |
|---|---|---|
| Nome sintético | Proc5 | NtObjectManager |
| Símbolo interno | EvtRpc_s_RegisterLogQuery | PDB de wevtsvc.dll |
| Nome | EvtRpcRegisterLogQuery | Especificação MS-EVEN6 |
O pipeline: WHEA, ETW e Windows Event Log
O Windows Hardware Error Architecture (WHEA) normaliza o tratamento de erros reportados por hardware, firmware e componentes do sistema. Quando uma condição é registrada, o Windows pode construir um WHEA_ERROR_RECORD composto por um cabeçalho, descritores de seção e seções de erro variáveis.
A documentação da Microsoft é explícita sobre o caminho normal desses dados: o kernel inclui o registro WHEA em um evento ETW (Event Trace Window) para que ele seja preservado no Event Log; uma aplicação em user mode pode recuperá-lo posteriormente para análise. Isso não é, por si só, um bug — é a função do subsistema. O problema começa quando o conteúdo persistido inclui valores de runtime cuja exposição não é necessária. Consulte WHEA Hardware Error Events e WHEA_ERROR_RECORD para mais informações.
O serviço Windows Event Log entrega o mesmo conjunto de eventos através do MS-EVEN6 e WinEvt.
As duas rotas, MS-EVEN6 e WinEvt, consomem o mesmo serviço Windows Event Log. Elas possuem, porém, caminhos de consumo suficientemente diferentes para responder a uma questão importante: o valor existe apenas como artefato de um cliente RPC artesanal ou também é recuperável pela abstração pública?
Há ainda uma restrição temporal. Arquivos EVTX sobrevivem a reinicializações. Um endereço contido em um registro antigo pode pertencer a um boot anterior e não revelar a base atual do kernel. Por isso, “endereço do kernel” e “primitiva de bypass do KASLR” são conclusões diferentes.
O método a seguir registra uma query sobre um canal ou arquivo de log:
error_status_t EvtRpcRegisterLogQuery(
[in, unique, string] LPCWSTR path,
[in, string] LPCWSTR query,
[in] DWORD flags,
[out, context_handle] PCONTEXT_HANDLE_LOG_QUERY *handle,
[out, context_handle] PCONTEXT_HANDLE_OPERATION_CONTROL *opControl,
[out] DWORD *queryChannelInfoSize,
[out] EvtRpcQueryChannelInfo **queryChannelInfo,
[out] RpcInfo *error
);
A descrição NDR extraída pelo NtObjectManager produziu o seguinte mapeamento:
O parâmetro _hProcHandle exibido pela ferramenta é o primitive binding handle. Ele representa a associação RPC já estabelecida e não aparece entre os argumentos semânticos passados a Proc5 no PowerShell.
| Parâmetro gerado | Semântica | Direção |
|---|---|---|
| p0 | path | in |
| p1 | query | in |
| p2 | flags | in |
| p3 | handle | out |
| p4 | opControl | out |
| p5 | queryChannelInfoSize | out |
| p6 | queryChannelInfo | out |
| p7 | error | out |
| retval | error_status_t | retorno |
A query usada na PoC foi:
$registration = $client.Proc5(
'Microsoft-Windows-Kernel-WHEA/Operational',
'*',
[uint32]0x00000101
)
| Flag | Valor | Semântica |
|---|---|---|
| EvtQueryChannelPath | 0x00000001 | path identifica um canal |
| EvtQueryForwardDirection | 0x00000101 | enumeração do mais antigo para o mais recente |
Em caso de sucesso:
if ([int]$registration.retval -ne 0) {
throw [System.ComponentModel.Win32Exception]::new(
[int]$registration.retval
)
}
$queryHandle = $registration.p3
$controlHandle = $registration.p4
Um retorno com o valor zero indica que a sintaxe foi aceita, o canal foi localizado e o access check de leitura foi satisfeito. Os dois valores devolvidos são context handles mantidos pelo servidor, não handles Win32 locais.
Opnum 11: EvtRpcQueryNext
Com o query handle aberto, EvtRpcQueryNext recupera o próximo batch (conjunto de dados do evento):
$batch = $client.Proc11(
$queryHandle,
[uint32]50,
[uint32]5000,
[uint32]0
)
Os argumentos solicitam até 50 registros, usam timeout de cinco segundos e as flags como zero.
Os principais campos de saída são:
| Campo gerado | Campo MS-EVEN6 |
|---|---|
| p4 | numActualRecords |
| p5 | eventDataIndices |
| p6 | eventDataSizes |
| p7 | resultBufferSize |
| p8 | resultBuffer |
resultBuffer é um blob contendo um ou mais eventos em BinXml. Os registros são empacotados sequencialmente; eventDataIndices[i] fornece o offset do evento “i” dentro do batch e eventDataSizes[i] fornece seu tamanho.
$offset = [int]$batch.p5[$i]
$size = [int]$batch.p6[$i]
$eventBytes = New-Object byte[] $size
[Array]::Copy(
[byte[]]$batch.p8,
$offset,
$eventBytes,
0,
$size
)
Respeitar esses limites evita que uma janela de oito bytes atravesse a fronteira entre dois registros e preserva a correlação entre candidato, evento e offset.
BinXml
O Event Log não precisa transportar cada registro como uma string XML completa. BinXml representa a árvore e seus valores com tokens binários e substituições tipadas.
Um campo renderizado como:
<Data Name="Srb">0xffffb80a80473408</Data>
Pode existir no registro como um inteiro de 64 bits em little-endian ou como parte de um payload binário maior.
A PoC percorre cada evento byte a byte e interpreta janelas de oito bytes como UInt64. O método é deliberadamente permissivo: offsets não alinhados ajudam a encontrar campos desconhecidos, mas também criam combinações sobrepostas e falsos positivos. Cada resultado deve, portanto, registrar o offset e se a posição era alinhada em oito bytes.
A filtragem usa duas classes:
- IMAGE-LIKE: endereço canônico da metade superior e dentro do intervalo heurístico 0xFFFFF80000000000–0xFFFFFEFFFFFFFFFF;
- KERNEL-LIKE: outro endereço canônico da metade superior.
Esses nomes são rótulos de triagem. IMAGE-LIKE não significa “confirmado em ntoskrnl.exe”, e KERNEL-LIKE não significa “confirmado como pool”. A atribuição real só ocorre depois da validação com o mapa de módulos da mesma execução.
O EvtRpcClose, opnum 13, remove context handles da tabela mantida pelo servidor:
$client.Proc13($queryHandle) | Out-Null
$client.Proc13($controlHandle) | Out-Null
$client.Disconnect()
Em uma execução curta, depender da ruptura da associação RPC pode parecer suficiente; em enumerações repetidas, a limpeza explícita evita manter estado desnecessário no svchost.exe.
Uma execução da PoC produziu a seguinte sequência de endereços:
Reprodução pela API pública WinEvt
Depois de observar os bytes no nível MS-EVEN6, escrevi uma segunda PoC em C usando apenas a API documentada do Windows Event Log. O objetivo não era criar uma “segunda fonte” de eventos, mas alterar a camada de consumo.
Abrindo e enumerando a query
EvtQuery abre um result set local:
EVT_HANDLE query = EvtQuery(
NULL,
L"Microsoft-Windows-Kernel-WHEA/Operational",
L"*",
EvtQueryChannelPath | EvtQueryForwardDirection
);
NULL seleciona o computador local; * seleciona todos os eventos. EvtNext devolve um array de EVT_HANDLE, um por registro:
EVT_HANDLE events[50];
DWORD returned = 0;
EvtNext(query, 50, events, 5000, 0, &returned);
Renderizando as propriedades do provider
O contexto EvtRenderContextUser seleciona os valores de EventData ou UserData:
EVT_HANDLE renderContext = EvtCreateRenderContext(
0,
NULL,
EvtRenderContextUser
);
Com EvtRenderEventValues, EvtRender devolve um array de EVT_VARIANT, não o batch BinXml bruto:
EvtRender(
renderContext,
event,
EvtRenderEventValues,
bufferSize,
buffer,
&bufferUsed,
&propertyCount
);
Os tipos relevantes são:
| Tipo | Interpretação |
|---|---|
| EvtVarTypeUInt64 | inteiro unsigned de 64 bits |
| EvtVarTypeHexInt64 | inteiro hexadecimal de 64 bits |
| EvtVarTypeBinary | payload binário |
| EvtVarTypeSizeT | valor de 32 ou 64 bits capaz de representar um endereço |
A enumeração oficial EVT_VARIANT_TYPE descreve EvtVarTypeSizeT explicitamente como um valor que contém um endereço.
O que a reprodução pelos dois caminhos demonstrou
| Características | Via RPC | API WinEvt |
|---|---|---|
| Interface visível ao cliente | MS-EVEN6 | WinEvt |
| Transporte visível | ncalrpc:[eventlog] | abstraído por wevtapi.dll |
| Unidade de enumeração | batch BinXml | EVT_HANDLE por evento |
| Representação usada | bytes e offsets | array de EVT_VARIANT |
| Cliente RPC gerado | necessário | não |
| ACL do canal aplicada pelo serviço | sim | sim |
O caminho RPC demonstra que os valores estão presentes nos bytes entregues pelo serviço. O caminho WinEvt demonstra que uma aplicação convencional também consegue alcançar dados equivalentes por meio das propriedades renderizadas.
Essa convergência permite excluir uma hipótese estreita: o achado não depende exclusivamente de um erro de unmarshalling no cliente que criamos. Ela não identifica, sozinha, a linha de código responsável.
As hipóteses restantes incluem:
- O provider registra intencionalmente um endereço em uma propriedade;
- Uma estrutura WHEA ou privada é persistida integralmente sem sanitização de valores de runtime;
- O payload binário contém bytes excedentes ou não inicializados;
- Uma etapa anterior à renderização transforma um campo legítimo em um endereço exposto.
A reprodução por EvtRenderContextUser enfraquece a hipótese de “memória residual exclusiva do batch RPC”, mas não prova qual das quatro alternativas é correta. A próxima etapa de root-cause analysis é mapear o índice do EVT_VARIANT para o nome da propriedade no template do provider e, no caso binário, identificar a estrutura e o offset exatos.
Conclusão
A investigação partiu de uma diferença entre apresentação e conteúdo: o Event Viewer não mostrava endereços, mas isso não respondia se os bytes ainda existiam em uma camada inferior.
A consulta direta à interface MS-EVEN6 mostrou que EvtRpcRegisterLogQuery e EvtRpcQueryNext entregavam registros BinXml contendo candidatos de 64 bits. A reprodução pela API WinEvt mostrou que dados equivalentes também estavam disponíveis a uma aplicação C convencional por meio de EvtRenderEventValues.
Dois candidatos foram confirmados dentro do intervalo de ntoskrnl.exe na execução analisada. Quando o evento pertence ao boot atual e o RVA é conhecido para o binário exato, um endereço desse tipo permite calcular a base da imagem e funciona como uma primitiva de bypass de KASLR.
O resultado mais importante, contudo, é metodológico. Uma mensagem renderizada é apenas uma visão do evento. Para investigar uma possível sanitização incompleta, é necessário seguir o dado por todas as camadas — provider, BinXml, serviço RPC e API pública — e manter separadas três coisas que frequentemente são confundidas: candidato, endereço validado e primitiva explorável.
A Microsoft corrigiu o CVE-2026-32217 nas atualizações cumulativas de 14 de abril de 2026. No Windows 11 24H2 e 25H2, a correção foi distribuída pelo KB5083769, elevando as builds para 26100.8246 e 26200.8246, respectivamente. Como as atualizações do Windows são cumulativas, releases posteriores também incorporam a correção.
Referências
- Yarden Shafir, KASLR Leaks Restriction.
- James Forshaw, Calling Local Windows RPC Servers from .NET.
- Microsoft, MS-EVEN: EventLog Remoting Protocol.
- Microsoft, MS-EVEN6: EventLog Remoting Protocol Version 6.0.
- Microsoft, EvtRpcRegisterLogQuery, opnum 5.
- Microsoft, EvtRpcQueryNext, opnum 11.
- Microsoft, EvtRpcClose, opnum 13.
- Microsoft, WHEA Hardware Error Events.
- Microsoft, Debugging Tools for Windows.
- Microsoft, Symbol path for Windows debuggers.
- Microsoft, Public Symbol Server.
- Microsoft, EvtQuery, EvtCreateRenderContext e EvtRender.
- Microsoft, EVT_VARIANT e EnumDeviceDrivers.
- Google Project Zero, NtObjectManager / Sandbox Attack Surface Analysis Tools.
- silverf0x, RpcView.














