Analisando um kernel address disclosure via Windows Event Log - CVE-2026-32217

CVE-2026-32217 Media
ProdutoMicrosoft Windows — serviço Windows Event Log (wevtsvc.dll)
ClasseDivulgação de endereço de kernel (bypass de KASLR)
VetorLocal · autenticado
CorreÇÃoAtualizações cumulativas de 14/04/2026

Author: Felipe Xavier
Introduçã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:

ProtocoloUUIDVersãoProcedures
MS-EVEN82273fdc-e32a-18c3-3f78-827929dc23ea0.027
MS-EVEN6f6beaff7-1e19-4fbb-9f8f-b89e2018337c1.029

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çãoNomeOrigem
Nome sintéticoProc5NtObjectManager
Símbolo internoEvtRpc_s_RegisterLogQueryPDB de wevtsvc.dll
NomeEvtRpcRegisterLogQueryEspecificaçã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 geradoSemânticaDireção
p0pathin
p1queryin
p2flagsin
p3handleout
p4opControlout
p5queryChannelInfoSizeout
p6queryChannelInfoout
p7errorout
retvalerror_status_tretorno

A query usada na PoC foi:

$registration = $client.Proc5(
    'Microsoft-Windows-Kernel-WHEA/Operational',
    '*',
    [uint32]0x00000101
)
FlagValorSemântica
EvtQueryChannelPath0x00000001path identifica um canal
EvtQueryForwardDirection0x00000101enumeraçã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 geradoCampo MS-EVEN6
p4numActualRecords
p5eventDataIndices
p6eventDataSizes
p7resultBufferSize
p8resultBuffer

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:

TipoInterpretação
EvtVarTypeUInt64inteiro unsigned de 64 bits
EvtVarTypeHexInt64inteiro hexadecimal de 64 bits
EvtVarTypeBinarypayload binário
EvtVarTypeSizeTvalor 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ísticasVia RPCAPI WinEvt
Interface visível ao clienteMS-EVEN6WinEvt
Transporte visívelncalrpc:[eventlog]abstraído por wevtapi.dll
Unidade de enumeraçãobatch BinXmlEVT_HANDLE por evento
Representação usadabytes e offsetsarray de EVT_VARIANT
Cliente RPC geradonecessárionão
ACL do canal aplicada pelo serviçosimsim

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