Saltar a contenido

CVE-2026-42980: reversing y explotación del underflow en WMI del kernel de Windows

Contexto de publicación

Esta investigación fue desarrollada como parte de Binary Gecko Academy. Ricardo Narvaja me autorizó a publicar este material a partir de hoy, 2026-07-07.

Este artículo, producido como parte de Binary Gecko Academy y publicado con permiso de Ricardo Narvaja a partir de hoy, documenta el root cause técnico y la estrategia de explotación de CVE-2026-42980, una vulnerabilidad de elevación local de privilegios en el kernel de Windows dentro del subsistema WMI. El advisory público indica el impacto y la clase de bug, pero no publica las funciones vulnerables, los IOCTLs ni la aritmética exacta. Este write-up completa esos detalles a partir de patch diffing, reversing estático y un exploit de laboratorio que llega a una shell interactiva NT AUTHORITY\SYSTEM.

Prueba de shell SYSTEM

TL;DR

Los sitios vulnerables están en las rutas de serialización WMI dentro de ntoskrnl.exe:

Camino IOCTL Función vulnerable Resta vulnerable
query de múltiples WMI data blocks 0x22812C nt!WmipQueryAllDataMultiple OutputBufferLength -= AlignedSize en WmipQueryAllDataMultiple+0x29a
execute/query de una single instance varias veces 0x228130 nt!WmipQuerySingleMultiple OutBufferSize -= AlignedActualSize en WmipQuerySingleMultiple+0x401

Ambos sitios mantienen un contador de 32 bits con el espacio restante del output. Los builds vulnerables restan un tamaño alineado reportado por el provider sin probar antes que ese tamaño sea menor o igual al contador restante. Si el tamaño es mayor, el contador unsigned wrappea a un valor enorme. La siguiente etapa de serialización WMI entonces cree que todavía hay espacio suficiente y escribe más allá del SystemBuffer kernel asignado.

El exploit usa el camino 0x228130 (nt!WmipQuerySingleMultiple) porque ofrece un trigger práctico in-box: la compuerta inicial de capacidad usa una estimación del tamaño del item WNODE controlada por el caller, mientras que la resta posterior usa el tamaño real serializado devuelto por el provider.

Patch diff: funciones vulnerables exactas

Al comparar el kernel vulnerable contra el parcheado, se ve que Microsoft cambió ambos caminos WMI de la misma manera: reemplazó la resta no validada por una resta saturada protegida por Feature_1045423416.

nt!WmipQueryAllDataMultiple (IOCTL 0x22812C)

El camino de query serializa múltiples bloques WMI y mantiene un remaining output length. En el build vulnerable, el loop hace esto:

AlignedSize = (QueryInfo[0] + 7) & 0xFFFFFFF8;
CurrentOutputBuffer = (char *)CurrentOutputBuffer + AlignedSize;
TotalSize += AlignedSize;
OutputBufferLength -= AlignedSize;  // vulnerable: resta unsigned sin check

A nivel instrucción, la operación vulnerable es:

nt!WmipQueryAllDataMultiple+0x29a:
    sub r14d, eax        ; r14d = remaining, eax = provider size alineado

El build parcheado cambia esto por una forma saturada:

v = OutputBufferLength - AlignedSize;
if (Feature_1045423416__private_IsEnabledDeviceUsageNoInline())
    v = -(unsigned int)(AlignedSize < OutputBufferLength) & v;
OutputBufferLength = v;

La máscara queda en 0xFFFFFFFF solamente cuando la resta no underflowea. Si AlignedSize >= OutputBufferLength, la máscara queda en cero y el contador restante se fuerza a 0 en vez de envolver.

nt!WmipQuerySingleMultiple (IOCTL 0x228130)

El camino explotado es el de single-instance/multiple-item detrás de:

\Device\WMIDataDevice
IOCTL_WMI_QUERY_SINGLE_MULTIPLE = 0x00228130

El build vulnerable contiene el mismo bug aritmético:

AlignedActualSize = (ReturnedDataSize + 7) & 0xFFFFFFF8;
TotalRequiredSize += AlignedActualSize;
OutBufferSize -= AlignedActualSize;  // vulnerable: resta unsigned sin check

El diff ubica el sitio vulnerable en:

nt!WmipQuerySingleMultiple+0x401

y la operación de bajo nivel es equivalente a:

sub dword ptr [rsp+4Ch], eax    ; outRemaining -= alignedActualSize

El build parcheado aplica el mismo patrón saturado:

t = OutBufferSize - AlignedActualSize;
if (Feature_1045423416__private_IsEnabledDeviceUsageNoInline())
    t = -(unsigned int)(AlignedActualSize < OutBufferSize) & t;
OutBufferSize = t;

Esto confirma que ambas funciones vulnerables pertenecen al mismo bug: una resta sin check de un tamaño alineado controlado por el provider contra un contador de output restante.

Alcanzabilidad desde user mode

Las rutas vulnerables son alcanzables desde un proceso local normal a través del dispositivo WMI:

[user mode]
  NtDeviceIoControlFile(\Device\WMIDataDevice, ioctl, ...)
      -> nt!WmipIoControl
          IOCTL 0x22812C -> nt!WmipQueryAllDataMultiple
          IOCTL 0x228130 -> nt!WmipQuerySingleMultiple
              -> nt!WmipQueryAllData / provider execution
              -> nt!WmipForwardWmiIrp
              -> WMI provider

Para el exploit final uso IOCTL 0x228130, porque el formato de request permite preparar dos WNODE inputs:

  1. item 0: crea el underflow contable;
  2. item 1: materializa la escritura out-of-bounds con bytes controlados.

El PoC abre \Device\WMIDataDevice directamente y también usa WmiOpenBlock / WmiQueryAllDataW desde advapi32.dll para descubrir instancias WMI vivas que tengan una ventana de trigger usable.

La aritmética exacta del trigger

La condición explotable en WmipQuerySingleMultiple nace de un desacople entre la estimación usada por la compuerta y el valor que se resta después.

Para cada item WNODE, la compuerta usa una estimación de tamaño de esta forma:

requiredSize = (DataSize + 73) & ~7;

Para un nombre de instancia WMI, esa estimación queda ligada al tamaño del item / nombre controlado por el caller. Sin embargo, después de ejecutar el camino del provider, el caller resta el tamaño real del WNODE serializado:

alignedActualSize = (ReturnedDataSize + 7) & ~7;
outRemaining -= alignedActualSize;

La relación explotable es:

requiredSize <= outRemaining
alignedActualSize > outRemaining

Eso significa que el item WNODE pasa el check inicial de capacidad, pero la resta posterior envuelve.

Una ventana concreta observada durante validación fue:

nameLen      = 0x4c
requiredSize = (0x4c + 0x49) & ~7 = 0x90
R            = 0x94
aligned      = 0x98
slop         = 4

gate:        0x90 <= 0x94       -> aceptado
subtract:    0x94 - 0x98        -> 0xFFFFFFFC

En otro build/perfil de laboratorio, el resolver dinámico eligió:

guid#156     = {2e2d2463-b537-4da7-8eee-51306f1f482f}
class        = WmiMonitorConnectionParams
nameLen      = 0x56
requiredSize = 0x98
R            = 0x9c
aligned      = 0xa0
slop         = 4
overwriteOff = 0xf1e

El GUID exacto no es el root cause. Lo importante es la ventana medida: ALIGN8(R) > R mientras la compuerta acepta el item.

Por qué el GUID puede cambiar

El PoC no depende de un provider mágico único. Incluye un dataset de GUIDs WMI y escanea dinámicamente ventanas usables en runtime. El provider elegido depende del build de Windows, perfil de VM, dispositivos habilitados, configuración de monitor, red y otros estados expuestos por WMI.

Ejemplos de providers exitosos o candidatos vistos en laboratorio:

índice GUID clase WMI
27 {0a214807-e35f-11d0-9692-00c04fc3358c} MSNdis_CoTransmitPduErrors
156 {2e2d2463-b537-4da7-8eee-51306f1f482f} WmiMonitorConnectionParams
463 {827c0a6f-feb0-11d0-bd26-00aa00b7b32a} MSPower_DeviceEnable
506 {8f680850-a584-11d1-bf38-00a0c9062910} MSSmBios_RawSMBiosTables
553 {98a2b9d7-94dd-496a-847e-67a5557a59f2} MS_SystemInformation
658 {bdd865d1-d7c1-11d0-a501-00a0c9062910} MSDiskDriver_Performance

Por eso el exploit calcula dinámicamente la geometría del overwrite. Si la ventana cambia de R=0x94/aligned=0x98 a R=0x9c/aligned=0xa0, el offset de overwrite también debe cambiar.

La fórmula central usada por el PoC es:

phase = (alignedActualSize + WMI_WNODE_INSTANCE_DATA_OFFSET) & (0x1000 - 1);
overwriteOffset = phase ? (0x1000 - phase) : 0;

con:

WMI_WNODE_INSTANCE_DATA_OFFSET = 0x42
WMI_POOL_CHUNK_STRIDE          = 0x1000

Para la ventana fallback 0x9c/0xa0, esto produce overwriteOff=0xf1e.

Convertir el wrap en escritura out-of-bounds

Después de que item 0 envuelve outRemaining, item 1 se procesa mientras el kernel cree que el output buffer tiene una capacidad enorme. El WNODE de item 1 se serializa fuera del SystemBuffer kernel asignado.

La parte útil es la copia de instance data del WNODE. En el exploit, el atacante controla los bytes copiados en:

OOB WNODE + 0x42

Por eso el código define:

#define WMI_WNODE_INSTANCE_DATA_OFFSET 0x42

El segundo WNODE se arma para que esa copia caiga sobre el header de una entrada vecina de named pipe.

Layout de pool y bucket matching

El objeto objetivo es una entrada de cola de datos de named pipe en npfs (NP_DATA_QUEUE_ENTRY, tag NpFr). El exploit crea muchas pipes y las llena para que el kernel reserve una secuencia estable de objetos de pipe del mismo tamaño.

Un detalle clave: la request WMI usa METHOD_BUFFERED. El kernel reserva el SystemBuffer del IOCTL con:

max(InputBufferLength, OutputBufferLength)

Si el SystemBuffer de WMI y las entradas de cola rociadas no caen en el mismo bucket de pool, la escritura OOB no aterriza confiablemente sobre el objeto objetivo. Por eso el exploit rellena el input length WMI:

WMI_SYSBUF_INLEN = 0xff0

y usa writes de pipe que producen objetos efectivos de 0x1000 bytes. La geometría importante es:

pipe data body        = 0xff0
data queue header     = 0x30
effective pool stride = 0x1000

En la calibración de bucket chico, la misma idea aparece así:

Sprayed NpFr DQE      = 0x30 + 0xb0 = 0xe0  -> chunk 0xf0
SystemBuffer before   = max(0x38, 0x94)     -> chunk 0xb0  (bucket incorrecto)
SystemBuffer after    = max(0xe0, 0x94)     -> chunk 0xf0  (bucket correcto)

El exploit público usa la geometría final grande 0xff0/0x1000.

Corrupción de la entrada de pipe

El primer trigger WMI corrompe la entrada de pipe objetivo e infla su tamaño legible. Luego PeekNamedPipe puede leer más allá de los datos originales de la pipe y filtrar metadata de kernel pool adyacente.

El exploit usa un offset estable de leak:

NPFS_LEGACY_LINKS_LEAK_OFFSET = 0xfd0

En ese offset extrae metadata de cola/lista de npfs necesaria para mantener consistente la estructura de la pipe mientras construye primitivas más fuertes.

Después del leak, el exploit vuelve a disparar WMI con otro payload para moldear la entrada corrupta como una entrada respaldada por IRP. Eso permite que PeekNamedPipe copie datos desde una dirección kernel elegida por el atacante, dando lectura arbitraria de kernel.

De lectura arbitraria a token replacement

Con lectura arbitraria de kernel, el exploit localiza el proceso actual y el proceso SYSTEM. Los offsets se seleccionan según el build de Windows. El camino de alto nivel es:

IRP
  -> ETHREAD mediante _IRP.Tail.Overlay.Thread
  -> EPROCESS actual mediante KTHREAD/ApcState.Process
  -> recorrido por ActiveProcessLinks
  -> EPROCESS de PID 4
  -> token de SYSTEM

Luego se usa una primitiva de escritura basada en completado de IRP para escribir el token de SYSTEM dentro del campo EPROCESS.Token del proceso actual. Después se repara la entrada de pipe corrupta y se lanza:

cmd.exe /k whoami

El proceso resultante corre como:

nt authority\system

Fases del PoC público

El PoC público imprime la cadena como fases:

  1. detectar build de Windows y seleccionar offsets de estructuras kernel;
  2. resolver NtFsControlFile, WmiOpenBlock y WmiQueryAllDataW;
  3. reservar buffers WMI;
  4. hacer spray de named pipes;
  5. llenar pipes para crear objetos de pool con el tamaño buscado;
  6. liberar una pipe para crear un hueco;
  7. abrir \Device\WMIDataDevice;
  8. resolver una ventana WMI y enviar IOCTL 0x228130;
  9. encontrar la pipe corrupta;
  10. filtrar metadata npfs;
  11. construir lectura arbitraria;
  12. localizar PID 4 y leer el token de SYSTEM;
  13. escribir el token de SYSTEM en el proceso actual;
  14. reparar el objeto pipe;
  15. spawnear la shell SYSTEM.

Compilación

Desde el directorio public/:

powershell -NoProfile -ExecutionPolicy Bypass -File .\build.ps1

o:

nmake /f Makefile

La salida queda en:

build\poc.exe

Impacto del parche

El parche corta la primitiva en la capa aritmética. Cuando el contador restante se satura a cero en vez de envolver, el siguiente WNODE no puede serializarse dentro del objeto de pool adyacente preparado por el atacante. Eso impide la corrupción de la cola de pipe, y por lo tanto también impide el leak, la lectura / escritura kernel y el token swap.

Binary Gecko Academy y permiso de publicación

Este trabajo fue producido como parte de Binary Gecko Academy. Lo publico con permiso de Ricardo Narvaja a partir de hoy, 2026-07-07.

Conclusión

CVE-2026-42980 no es simplemente “un bug en WMI”. Las funciones vulnerables son nt!WmipQueryAllDataMultiple y nt!WmipQuerySingleMultiple; el IOCTL explotado es 0x228130; y la primitiva clave es una resta sin check de un tamaño alineado contra un contador de 32 bits de output restante. La explotabilidad aparece al combinar ese wrap aritmético con instance data WNODE controlada y grooming preciso de pool con npfs. El resultado final es una elevación local confiable a SYSTEM en las configuraciones vulnerables de laboratorio probadas.