Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2020-1206 | Kitploit
Herramientas/GitHubGitHub/datntsec/cve-2020-1206
Forensia de MemoriaAnálisis de VulnerabilidadesExplotaciónRecopilación de InformaciónPruebas de PenetraciónExplotación de Binarios
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

Ver Repositorio
hace 5 añosAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

En la vulnerabilidad SMBGhost (CVE-2020-0796) hablé sobre una técnica de primitiva write-what-where mediante el uso de una vulnerabilidad de desbordamiento de enteros para cambiar el puntero Alloc.Userbuffer para que apunte a una dirección deseada y escribir datos arbitrarios en ella. Al igual que SMB Ghost, esta vulnerabilidad también existe en la función Srv2DecompressData en srv2.sys. Repasemos la función Srv2DecompressData relacionada con la vulnerabilidad SMBGhost (CVE-2020-0796) simplificada por Zecops``` c typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;

typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER;

NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; }

root@kitploit:~
ULONG FinalCompressedSize = 0;


NTSTATUS Status = SmbCompressionDecompress(
    Header->CompressionAlgorithm,
    (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
    (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
    (PUCHAR)Alloc->UserBuffer + Header->Offset,
    Header->OriginalCompressedSegmentSize,
    &FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}


if (Header->Offset > 0) {
    memcpy(
        Alloc->UserBuffer,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
        Header->Offset);
}


Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;

}

root@kitploit:~
La función Srv2DecompressData recibe un mensaje comprimido enviado por el cliente y procede a asignar la memoria necesaria, descomprimiendo el mensaje en ella. Después, si el campo Offset es distinto de cero, copia los datos (RawData) que preceden a los datos comprimidos al comienzo de la memoria asignada.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

El fallo de SMBGhost radica en que la función no comprueba el desbordamiento de enteros (integer overflow), lo que provoca una asignación de tamaño incorrecto y causa un desbordamiento de búfer (buffer overflow). Tres meses después de que Microsoft parcheara SMBGhost, se encontró la vulnerabilidad CVE-2020-1206 (SMBleed, como la denomina [Zecops Blog](https://blog.zecops.com/)). Este fallo nos permite filtrar (leak) la dirección de otra máquina y, si se combina con SMBGhost, podemos obtener RCE. Para tener una visión más sencilla de la función Srv2DecompressData, reutilizaremos esta función tal como estaba antes del parche de SMBGhost y supondremos que ya ha sido parcheada.

# Falsificación de OriginalCompressedSegmentSize
Al igual que con SMBGhost, en esta ocasión seguiremos falsificando OriginalCompressedSegmentSize con un número ligeramente mayor que los datos descomprimidos que enviamos. Por ejemplo, si comprimimos datos de x bytes, en lugar de poner x en el campo OriginalCompressedSegmentSize, pondremos x + 0x1000; esto se aclara en la siguiente imagen:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

Los datos no inicializados del kernel se tratarán como parte del mensaje.

Como dije en el análisis de [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796), Srv2DecompressData seguirá omitiendo la fase de comprobación posterior a la función SmbCompressionDecompress si la descompresión se realiza correctamente:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}

Aunque el campo OriginalCompressedSegmentSize se establece en x + 0x1000 en lugar de x, después de una descompresión exitosa, la variable FinalCompressedSize no contiene el valor x, sino que contendrá el valor x + 0x1000:```c NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
Debido a que después de una descompresión exitosa, FinalCompressedSize se actualiza para contener el valor de CompressedBufferSize (correspondiente a OriginalCompressedSegmentSize pasado a la función SmbCompressionDecompress). La actualización y la comprobación posterior son casi innecesarias, lo que puede provocar algunos errores inesperados.

# Explotación básica

La estructura de mensaje que Zecops utiliza para demostrar la vulnerabilidad es el [mensaje SMB2 WRITE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). Esta estructura contiene campos como el número de bytes que se pueden escribir, flags, etc., seguidos de un búfer de longitud arbitraria. Esto es bastante perfecto para explotar el error, ya que podemos crear un mensaje y especificar el encabezado, con un búfer que contiene datos no inicializados.

Basándonos en el POC de [Zecops](https://blog.zecops.com/) en el repositorio WindowsProtocolTestSuites de Microsoft, para tener una visión más clara de esto, añadiremos esta pequeña adición a la función de compresión:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
    ((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
    compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}

Tenga en cuenta que este PoC requiere credenciales y permisos de escritura compartidos, que suelen estar disponibles en muchos casos. Sin embargo, el error devuelto se aplica a todos los mensajes (incluidos los mensajes con o sin credenciales), por lo que es posible que podamos explotarlo sin necesidad de autenticación. Además, la memoria que filtraremos proviene de asignaciones anteriores en NonPagedPoolNx, y dado que podemos controlar el tamaño de la asignación, controlaremos en cierta medida los datos que filtraremos.

SMBleed POC Source Code

Entonces, ¿es posible filtrar una dirección del kernel sin credenciales? Para responder a esta pregunta, analicemos SMB más a fondo.

Profundizando en SMB

Al autenticarse, el cliente envía los siguientes mensajes:

SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP

Si las credenciales son incorrectas, la sesión se termina después del segundo paquete SMB2 SESSION_SETUP:

Supongamos que no tenemos credenciales; comprobaremos si hay algún comando que pueda enviarse sin necesidad de autenticación. Tras buscar, observamos:

  • El primer comando que debe enviarse es SMB2 NEGOTIATE y también es el único comando SMB2 NEGOTIATE durante toda una sesión.
  • Los comandos siguientes, hasta que la autenticación tenga éxito, deben ser SMB2 SESSION_SETUP.

El mensaje SMB2 NEGOTIATE no se comprime. El bug está en la función de descompresión, por lo que no lo consideraremos y solo examinaremos los mensajes SMB2 SESSION_SETUP.

SMB2 SESSION_SETUP

Como se mencionó anteriormente, una sesión normal envía 2 comandos SMB2 SESSION_SETUP. Los paquetes de respuesta no contienen datos necesarios para explotar, y tampoco tenemos forma de influir en el paquete de respuesta. Sin embargo, la segunda respuesta tiene un cuerpo vacío con el estado 0xC000006D (STATUS_LOGON_FAILURE) en el encabezado del paquete. Observamos que el primer paquete SMB2 SESSION_SETUP contiene la solicitud NTLM Negotiate message y el segundo contiene el NTLM Authenticate message. El mensaje NTLM Negotiate es bastante simple y probablemente no tenga nada interesante, así que profundizaremos en el mensaje NTLM Authenticate.

NTLM Authenticate message

Después de estudiar el mensaje NTLM Authenticate, observamos que la parte más compleja de este mensaje, y la más adecuada para explotar, es la estructura NTLM2 V2 Response. Esta estructura es una matriz de bytes de tamaño variable que contiene principalmente la estructura NTLMv2_CLIENT_CHALLENGE. Vemos que si esta estructura no pasa las comprobaciones iniciales, se devuelve el valor 0xC000000D (STATUS_INVALID_PARAMETER) en lugar de 0xC000006D (STATUS_LOGON_FAILURE). Una de las comprobaciones iniciales es la verificación del campo AvPairs.

El campo AvPairs es una matriz de bytes de tamaño variable que contiene estructuras AV_PAIR. Cada AV_PAIR define un par atributo/valor; el atributo está definido por el campo AvId, el campo AvLen define la longitud en bytes del valor, y el campo Value es una matriz de bytes de tamaño variable que contiene el valor propiamente dicho. Un elemento con el atributo MsvAvEOL y longitud cero marca el final de la matriz.

El mensaje Authenticate es procesado por la función SsprHandleAuthenticateMessage del módulo msv1_0.dll. En las comprobaciones iniciales, esta función se asegura de que la matriz AvPairs contenga los siguientes atributos: 0x0001 (MsvAvNbComputerName), 0x0002 (MsvAvNbDomainName). Sin embargo, su valor no se comprueba; solo se recorre la matriz y se verifica si el atributo solicitado existe y si su longitud está dentro de la estructura. Si la longitud es demasiado grande, el recorrido se detiene. Por lo tanto, en la práctica, no se comprueba si MsvAvEOL es válido.

En este punto, hemos descubierto que podemos crear una solicitud que nos ayude a responder la siguiente pregunta: dados dos bytes en el offset x, de tipo uint16, ¿su valor es mayor que y? Tanto x como y están bajo nuestro control. Consideremos el siguiente paquete:

El contenido del valor 0x0001 (MsvAvNbComputerName) no es importante, por lo que podemos usarlo para ajustar el offset del segundo valor. Para el segundo valor, solo establecemos el atributo como 0x0002 (MsvAvNbDomainName), sin inicializar len ni value. También establecemos el tamaño de todo el paquete para que haya y bytes según el campo length. Hay dos resultados posibles dependiendo del valor no inicializado del campo length del segundo valor:

  • length <= y: En este caso, la comprobación se supera porque se encuentra el valor 0x0002 válido (MsvAvNbDomainName). El servidor devuelve 0xC000006D (STATUS_LOGON_FAILURE) porque las credenciales son incorrectas.
  • length > y: En este caso, la comprobación falla, porque el segundo valor tiene una longitud no válida y se descarta. El servidor devuelve 0xC000000D (STATUS_INVALID_PARAMETER) en este caso.

Según la respuesta del servidor, siempre podemos deducir la respuesta a la pregunta anterior.

Sin embargo, el mensaje NTLM Authenticate está limitado a 0xB48 bytes y se descarta si es mayor. Esta comprobación la realiza la función SspContextGetMessage del módulo msv1_0.dll. Entonces, supongamos que solo escribimos 1 byte de len; el byte restante contendrá un valor no inicializado. ¿Podríamos omitir esta limitación? Lamentablemente no, porque el valor uint16 está codificado en little endian. Por lo tanto, no podemos lograr lo que queremos en una sola sesión SMB; pasaremos a examinar otros factores.

Observación #1: Listas de lookaside

Como se mencionó en la investigación anterior (CVE-2020-0796), los módulos del kernel que procesan SMB (srv2.sys y srvnet.sys) utilizan una función de asignación personalizada, SrvNetAllocateBuffer, exportada por srvnet.sys. Esta función usa listas de lookaside para optimizar las asignaciones pequeñas. Las listas de lookaside se utilizan para almacenar eficientemente un conjunto de búferes de tamaño fijo reutilizables para el controlador.

Las listas de lookaside se crean durante la inicialización; las listas para cada tamaño y procesador lógico se describen en la siguiente tabla:

Cada celda con el símbolo "📝" es una lista de lookaside independiente. Para simplificar el análisis, asumiremos que nuestro objetivo tiene un solo procesador lógico. En este caso, mientras se asigne la misma cantidad de bytes y se use la misma lista de lookaside, se reutilizará el mismo búfer en múltiples ocasiones. Podemos usar esto para tener cierto control sobre los datos no inicializados.

Observación #2: Falla de la descompresión

Revisemos qué sucede cuando se descomprime un paquete comprimido (consulte el write-up CVE-2020-0796 para más detalles y pseudocódigo):

En el caso de que CompressedData no sea válido, la fase de descompresión falla, la fase de copia no se ejecuta y la conexión se interrumpe. Pero la descompresión puede fallar solo después de descomprimir parte de los CompressedData válidos. Esto nos permite crear una solicitud de modo que los datos que elijamos se escriban en el offset que elijamos, como se muestra a continuación:

De vuelta al mensaje NTLM Authenticate

Podemos usar las observaciones anteriores para hacer que nuestra técnica funcione mediante dos pasos:

  1. Enviar un mensaje con datos comprimidos no válidos para que solo se descomprima un único byte 0. Ese byte será el primer byte del campo length del segundo Value en la matriz AvPairs.
  2. Enviar un mensaje similar al anterior, pero asegurándose de que se use la misma lista de lookaside para la asignación, de modo que el byte 0 esté allí.

Esta vez, la técnica puede responder la siguiente pregunta: dado un byte en el offset x, ¿su valor es mayor que y? Como antes, x e y están bajo nuestro control.

Como podemos reutilizar el búfer varias veces asegurándonos de usar la misma lista de lookaside, podemos repetir los pasos varias veces cambiando y, y finalmente deducir el valor del byte en un desplazamiento determinado.

Sin embargo, esta técnica tiene una limitación: el offset del byte que podemos leer está limitado al byte 0xADB desde el inicio del búfer del paquete. Esto se debe a que el offset del mensaje NTLM Authenticate (AUTHENTICATE_MESSAGE) está limitado a 0x40 bytes después del final de los encabezados SMB2 SESSION_SETUP (aplicado por la función Smb2ValidateSessionSetup en srv2.sys) y el tamaño del mensaje NTLM Authenticate (AUTHENTICATE_MESSAGE) está limitado a 0xB48 bytes. Buscaremos la forma de resolver esto.

Supongamos que queremos leer un byte en el offset 0x1100. No podemos hacerlo directamente con la técnica anterior; sin embargo, aún podemos usar la siguiente técnica adicional: dado que los búferes se reutilizan de las listas de lookaside, podemos "elevar" el byte objetivo mediante la función de descompresión estableciendo el campo Offset para sobrepasarlo. Solo necesitamos asegurarnos de que los datos allí presentes puedan interpretarse como datos comprimidos válidos; de lo contrario, la copia no se producirá.

El búfer del paquete que contiene los datos enviados por el cliente incluirá 16 bytes adicionales de encabezados que no se copian durante la descompresión. Como resultado, los datos copiados y descomprimidos, incluido el byte objetivo, se copian a una posición 16 bytes más cerca del inicio del búfer asignado. Podemos repetir esto varias veces, hasta que el offset del byte objetivo sea suficientemente bajo.

POC de fuga de direcciones

Puedes encontrar un script que demuestra la técnica anterior aquí. Recuerda que asumimos que el servidor tiene un solo procesador lógico, por lo que deberás configurar tu máquina virtual adecuadamente para que el script funcione. Si todo sale bien, el script leerá y filtrará la dirección del pool NonPagedPoolNx. En realidad, será la dirección de uno de los búferes que se encuentran en las mismas listas de lookaside.

Debido a que esta técnica tiene bastantes limitaciones, no la analizaré más a fondo. No obstante, puedes leer el script anterior y analizarlo por tu cuenta.

Un enfoque diferente: descompresión

Durante la investigación, Zecops se dio cuenta de que el paquete SMB descomprimido no es la única estructura compleja que puede ser no válida de diferentes maneras. Incluso antes de procesar todas las estructuras relacionadas con SMB, el búfer comprimido también puede ser no válido. Si la descompresión falla, la conexión con el servidor se interrumpe.

Microsoft ofrece tres algoritmos de compresión para elegir al implementar SMB: LZNT1, Plain LZ77 y LZ77 + Huffman. Solo consideraremos LZNT1 porque es bastante simple: alrededor de 80 líneas de Python para una función de descompresión. Hablaré brevemente sobre el proceso de descompresión: los datos comprimidos consisten en una secuencia de bloques comprimidos, cada bloque comienza con una variable uint16 que marca la longitud de dicho bloque. Cuando se encuentra una longitud igual a 0, el proceso de descompresión finaliza. Usaremos esto para escribir una secuencia de bytes 0 que represente datos comprimidos válidos. El propósito es responder la pregunta anterior: dado un byte en el offset x, ¿su valor es mayor que y? Por supuesto, x e y seguirán bajo nuestro control.

A continuación se muestra un ejemplo de los datos comprimidos que enviaremos:

Hay dos resultados posibles dependiendo del valor no inicializado del primer byte del campo length:

  • length <= y: En este caso, el primer bloque será todo bytes 0, lo cual es completamente válido, y la longitud del siguiente bloque será 0, por lo que la descompresión finalizará. El servidor devolverá una respuesta.
  • length > y: En este caso, el primer o segundo bloque comprimido contendrá bytes 0xFF; este bloque no podrá descomprimirse. El servidor cerrará la conexión debido a que los datos comprimidos no son válidos.

Al igual que con la técnica anterior, podemos usar las observaciones 1 y 2 para crear un mensaje con un byte no inicializado en medio del mensaje usando dos pasos:

  1. Enviar un mensaje con datos comprimidos no válidos para que solo se descomprima una parte de los datos, similar a la imagen anterior.
  2. Enviar el segundo mensaje y asegurarse de que se use la misma lista de lookaside que en el primer mensaje, para que los bytes del paso 1 estén allí.

Observa que el valor Offset en el encabezado del paquete SMB apuntará a los datos comprimidos; estos datos pueden ser válidos o no dependiendo del valor del byte no inicializado.

La ventaja más notable de esta técnica sobre la anterior es que ya no hay límite de offset.

En resumen, tenemos dos técnicas para leer una región de memoria no inicializada del búfer del pool asignado por la función SrvNetAllocateBuffer del módulo srvnet.sys. La primera técnica crea un paquete SMB especial y luego deduce la información a través de la respuesta del servidor. En la segunda técnica, con menos limitaciones, creamos datos comprimidos especiales y los enviamos, para luego deducir la información según si el servidor cierra la conexión o no.

Por lo tanto, podemos usar una de las dos técnicas anteriores para explotar. Y como dije, la primera técnica tiene muchas limitaciones, así que solo profundizaremos en la segunda.

Esta técnica nos ayudará a explotar la primitiva write-what-where que Zecops demostró anteriormente en su investigación previa sobre cómo lograr una escalada de privilegios local. Usaremos esta técnica para filtrar direcciones del diseño de memoria y así poder usar la primitiva write-what-where. Desafortunadamente, la memoria asignada por la función SrvNetAllocateBuffer se usa principalmente para datos de red, como paquetes SMB, y no contiene ningún puntero del sistema. Y debido a que necesitamos lograr RCE, filtrar regiones de memoria no inicializadas de asignaciones anteriores de SrvNetAllocateBuffer es inútil, ya que no hay certeza sobre la ubicación de los punteros que buscamos. Necesitamos encontrar algo más útil.

SrvNetAllocateBuffer y la disposición del búfer asignado

Como mencioné en la investigación sobre escalada de privilegios local (CVE-2020-0796), la función SrvNetAllocateBuffer no solo devuelve un búfer con el tamaño solicitado. En su lugar, devuelve un puntero que apunta a la región justo debajo del búfer de usuario del bloque de memoria asignado del pool, que contiene información sobre el búfer asignado. La disposición del bloque de memoria asignado del pool es la siguiente:

Aunque nuestra técnica de lectura solo puede leer bytes de la región "User Buffer", aún podemos usar otra técnica para copiar partes de la estructura SRVNET_BUFFER_HDR al "User Buffer" de otro búfer para poder leerla. Para ello, establecemos el campo Offset para que apunte a la estructura SRVNET_BUFFER_HDR que está fuera de los datos que queremos leer. Solo necesitamos asegurarnos de que los datos allí presentes puedan interpretarse como datos comprimidos válidos; de lo contrario, la copia no se realizará.

En busca de punteros

Examinemos los campos de la estructura SRVNET_BUFFER_HDR y veamos si hay algún contenido que valga la pena leer:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
Los punteros `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` son punteros que apuntan al interior del bloque de memoria asignado del pool, con offsets que pueden calcularse de antemano, por lo que solo necesitamos leer uno de ellos. Tener un puntero que apunte al bloque de memoria asignado del pool definitivamente nos ayudará en la explotación. Además, los siguientes punteros también son muy importantes:
- **ConnectionBufferList**: Una lista enlazada de todos los buffers recibidos pero aún no procesados de una conexión. La cabeza de esta lista es un objeto de conexión creado por la función SrvNetAllocateConnection en srvnet.sys. Un buffer se añade a la lista mediante la función SrvNetWskReceiveComplete. En nuestro caso, solo habrá un único buffer en la lista, por lo que ambos punteros (Flink y Blink de la estructura LIST_ENTRY) apuntarán a la cabeza de la lista dentro del objeto de conexión.
- **pSrvNetWskStruct**: Inicialmente, un puntero al objeto de conexión mencionado anteriormente. El puntero es establecido por la función SrvNetWskReceiveEvent, pero es sobrescrito por la función SrvNetWskReceiveComplete con un puntero a la estructura SRVNET_BUFFER_HDR. Por lo tanto, leerlo no es más útil que leer uno de los cuatro punteros mencionados. Por cierto, si buscas “pSrvNetWskStruct”, verás que tiene un papel en la explotación de EternalBlue.
- **TracingPtr1/2**: Estos punteros solo se utilizan cuando la función de tracing está activada.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Como puedes ver, el único otro puntero útil que podemos leer es un puntero dentro de la estructura ConnectionBufferList. Ambos punteros (Blink y Flink en la estructura LIST_ENTRY) apuntan al objeto de conexión. Este objeto fue nombrado SRVNET_RECV por el investigador de EternalBlue, así que también usaremos ese nombre.

## Obtención de la dirección base de un módulo
Ahora que sabemos cómo obtener dos punteros — uno que apunta al bloque de memoria asignado del pool y otro que apunta a la estructura SRVNET_RECV — podemos modificar libremente los dos buffers utilizando la primitiva write-what-where. Puede haber muchas formas de lograr RCE, pero obtener una dirección base de un módulo será la opción más sencilla porque hay muchas cosas que podemos modificar en la sección de datos de un módulo. Como hemos visto, ningún puntero dentro del bloque de memoria asignado por SrvNetAllocateBuffer apunta a un módulo. Sin embargo, todavía hay algunos punteros que apuntan a los módulos:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

La técnica de lectura que tenemos solo nos permite leer datos en la región "User Buffer", mientras que estos punteros están bastante lejos y son apuntados por bastantes otros punteros. Necesitamos un fragmento de código que pueda hacer lo siguiente para copiar el valor del puntero a la región "User Buffer":``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

Si pudiéramos encontrar un fragmento de código así, lo activaríamos para copiar el primer puntero (por ejemplo, HandlerFunctions) en el "User Buffer", leerlo, luego copiar el segundo puntero (por ejemplo, el puntero a la función Srv2ConnectHandler) en el "User Buffer" y leerlo, deduciendo la dirección base del módulo a partir de él. El equipo de Zecops buscó un fragmento de código así durante mucho tiempo, pero no encontró uno adecuado. Finalmente, utilizaron una alternativa relacionada con la función SrvNetFreeBuffer (simplificada a continuación) que tiene una funcionalidad casi deseada:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
Al liberar el búfer, si los flags del búfer son 0x02 (lo que significa que el búfer forma parte de una lista lookaside) y 0x01 (lo que significa que el búfer no tiene encabezado de transporte) están establecidos, se realizan algunas operaciones sobre dos objetos MDL para añadir el encabezado de transporte antes de restablecer los flags a 0 y devolver el búfer a la lista lookaside. Si examinamos de cerca detrás de las operaciones sobre los objetos MDL, podemos notar que el código realiza una doble desreferencia de lectura seguida de una doble desreferencia de escritura con dos variables que controlamos (dos punteros MDL), que es lo que estamos buscando. La desventaja es que el contenido que queremos leer también se modifica, un efecto secundario que esperamos poder evitar.

Con lo anterior, así es como logramos leer el puntero AcceptSocket:

1. Preparar el búfer A desde una lista lookaside de modo que la región “User buffer” esté llena de ceros. La región de búfer de usuario de este búfer contendrá el puntero que leeremos.
2. Preparar el búfer B desde otra lista lookaside de modo que:
   - El puntero pMdl1 apunte a la dirección del puntero AcceptSocket menos 0x18 (debido a que el desplazamiento de MappedSystemVa es 0x18 en la estructura MDL).
   - El puntero pMdl2 apunte a la región “User buffer” del Búfer A.
   - El campo Flags esté establecido a 0x03.

Podemos sobrescribir los campos de la estructura SRVNET_BUFFER_HDR desempaquetándolos desde un búfer más grande mediante la técnica descrita en la sección [Observación #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) anterior.

3. Cuando se libera el Búfer B, tendrán lugar las siguientes operaciones:
   - Los flags MDL se leerán del segundo MDL en el búfer A. Si el flag MDL_PARTIAL_HAS_BEEN_MAPPED está establecido, se llamará a MmUnmapLockedPages y el sistema podría bloquearse. Por eso tenemos que llenar el búfer con ceros en el paso 1.
   - El puntero AcceptSocket y la memoria circundante se modificarán como se describe aquí.```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • Con trỏ AcceptSocket và bộ nhớ xung quanh nó sẽ được đọc như mô tả ở đây:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- Vùng “User buffer” của buffer A sẽ được sửa đổi như được mô tả ở đây: (Các byte màu cam chứa con trỏ mà chúng ta muốn đọc, chúng ta chỉ cần sắp xếp chúng đúng cách)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. Leer el puntero AcceptSocket desde la región “User buffer” del búfer A.

La buena noticia es que hemos leído el puntero. La mala noticia es que hemos dañado algunos datos dentro de la estructura SRVNET_RECV. Por suerte para nosotros, el error no afecta al sistema siempre que no ocurra nada con la conexión implicada. Cuando algo ocurre, por ejemplo, el cierre de la conexión, el sistema se bloqueará (crash). Eso no es un problema porque pronto tendremos RCE y podemos corregir el error si queremos.

Después de leer el puntero AcceptSocket, continuamos usando la misma técnica para leer el puntero srvnet!SrvNetWskConnDispatch. La razón por la que leemos el puntero AcceptSocket y no el puntero HandlerFunctions es que la matriz de HandlerFunctions se comparte entre todas las conexiones, mientras que el búfer al que apunta AcceptSocket no se comparte con otras conexiones. Por lo tanto, si dañamos partes de AcceptSocket, solo afectará a la estabilidad de una conexión.

Si tenemos una copia del archivo srvnet.sys utilizado en la máquina objetivo, podemos deducir fácilmente la dirección base del módulo srvnet.sys restando el offset del puntero SrvNetWskConnDispatch que hemos filtrado (leaked).

Implementando lectura arbitraria

Supongamos que tenemos la dirección base del módulo srvnet.sys; podemos llamar a cualquier función del módulo. Pero, ¿qué pasa con los argumentos de la función? La función srv2!Srv2ReceiveHandler es llamada por SrvNetCommonReceiveHandler y la llamada tiene la siguiente forma:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
Los dos primeros argumentos se leen de la estructura SRVNET_RECV, por lo que podemos controlarlos, pero no podemos controlar los argumentos restantes. La convención de llamada x86-64 especifica que el llamador es responsable de asignar y liberar el espacio de pila para los argumentos, así que aunque estaba previsto llamar a una función de 8 argumentos, podemos reemplazar el puntero por cualquier otra función que esperemos.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

A continuación se muestran los pasos que utilizaremos para desencadenar la llamada a la función:
1. Enviar un mensaje especialmente diseñado para que el puntero a la estructura SRVNET_RECV de la conexión se copie en un búfer que podamos leer.
2. Enviar otro mensaje válido que reutilice la misma estructura SRVNET_RECV, pero sin cerrar la conexión. Nótese que cuando la conexión se cierra, la estructura SRVNET_RECV no se libera. Se llama a la función SrvNetPrepareConnectionForReuse para restablecer la estructura de modo que pueda reutilizarse para la siguiente conexión.
3. Leer el puntero a la estructura SRVNET_RECV que copiamos en el paso 1.
4. Reemplazar el puntero HandlerFunctions y los argumentos usando la técnica de primitiva write-what-where.
5. Enviar un mensaje adicional a través de la conexión del paso 2 para que se invoque la función sustituta de srv2!Srv2ReceiveHandler.

Ahora todo lo que tenemos que hacer es encontrar una función que copie memoria de un lugar a otro, para poder copiar memoria arbitraria a un búfer de pool desde el que podamos leer. memcpy es una opción y srvnet.sys tiene una función así (más exactamente, memmove), pero esta función requiere un tercer argumento, el que determina el número de bytes a copiar, y no podemos controlarlo. Sin embargo, no estamos limitados a las funciones implementadas en srvnet.sys; también podemos llamar a funciones de la tabla de importación de srvnet, y RtlCopyUnicodeString es una opción perfecta para lograr lo que queremos.

La función RtlCopyUnicodeString toma dos punteros a UNICODE_STRING como argumentos y copia el contenido de la cadena de origen a la cadena de destino. A diferencia de las cadenas C terminadas en NULL, las cadenas en el kernel se definen mediante la estructura UNICODE_STRING, que contiene un puntero a la cadena y la longitud de la cadena en bytes. El búfer de la cadena puede contener cualquier dato binario. Si observa el código de la función RtlCopyUnicodeString, puede ver que la copia se realiza con la función memmove, es decir, una copia de datos binarios puros. Todo lo que tenemos que hacer es preparar dos estructuras UNICODE_STRING y llamar a RtlCopyUnicodeString, y luego leer los datos copiados:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## Ejecutando shellcode
Una vez conseguida una práctica primitiva de lectura arbitraria, pasamos al siguiente reto hacia el objetivo de ejecución remota de código mediante la ejecución de un shellcode. Usaremos la técnica presentada por Morten Schenk en su charla de [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) (páginas 47-51).

La idea es escribir un shellcode por debajo de la estructura KUSER_SHARED_DATA, cuya dirección es constante, la única dirección no aleatorizada en el diseño de memoria del kernel de las versiones recientes de Windows. Luego, modificar la entrada de la tabla de páginas (page table entry) correspondiente para que la página sea ejecutable. La dirección base de las entradas de la tabla de páginas en el kernel es aleatoria, pero se obtiene de la función MiGetPteAddress en ntoskrnl.exe. A continuación se muestran los pasos que usaremos para ejecutar nuestro shellcode:
1. Usar la primitiva de lectura arbitraria para obtener la dirección base de ntoskrnl.exe desde la tabla de importación de srvnet.
2. Leer la dirección base de la entrada de la tabla de páginas de la función MiGetPteAddress, como se describe en las diapositivas de Morten.
3. Escribir el shellcode en la dirección KUSER_SHARED_DATA + 0x800 (0xFFFFF78000000800). Nótese que también podríamos usar uno de los búferes de pool para almacenar el shellcode; usar KUSER_SHARED_DATA es para simplificar las cosas.
4. Calcular la dirección de la entrada de la tabla de páginas correspondiente y borrar el bit NX para permitir la ejecución, como se describe en las diapositivas de Morten.
5. Invocar el shellcode usando la técnica presentada anteriormente para llamar a una función arbitraria.

El shellcode que Zecops usa para la reverse shell es el [shellcode de sleepya](https://github.com/worawit/MS17-010/tree/master/shellcode), escrito para el exploit EternalBlue. Modificaron ese shellcode para que funcionara en versiones recientes de Windows.

# Depuración

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

La información de un paquete SMB tendrá una estructura muy parecida a la anterior. Supongamos que necesitamos filtrar (leak) la dirección del búfer de usuario; tendremos que leer el puntero UserBufferPtr. Para leer este puntero, aprovecharemos la [técnica](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout) de colocar el campo de offset más allá del puntero, de modo que se copie en el búfer de usuario de otro búfer.

Un ejemplo de paquete SMB enviado por el cliente con el siguiente contenido:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

Este paquete, al llegar al servidor, se almacenará en un buffer creado por la función SrvNetAllocateBuffer. Debido a que el tamaño total del paquete está en el rango de 0x1100 a 0x2100, esta función devolverá un alloc con una zona de user buffer de tamaño 0x2100 (lo llamaremos Alloc A), y luego almacenará la información enviada por el cliente como se muestra en la siguiente imagen:

Podemos ver que la parte desde la dirección 0xffffd38439044050 hasta 0xffffd38439045160 son datos enviados por el cliente, la parte desde 0xffffd38439045160 hasta 0xffffd38439046150 son datos no inicializados en el lado del servidor, y la parte desde 0xffffd38439046150 hasta 0xffffd38439046240 son datos del SRVNET_BUFFER_HDR de Alloc A. Por lo tanto, el puntero que queremos leer estará en 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

Para leer este puntero, utilicé la técnica que mencioné anteriormente, estableciendo el campo offset más allá del puntero que se desea leer. Por eso, aunque el paquete anterior tiene un tamaño menor a 0x2100, el offset se establece en 0x2116.

A continuación, el servidor SMB llamará a la función SrvNetAllocateBuffer para asignar una región de memoria basada en la suma de OriginalCompressedSegmentSize y Offset (0x2116) . De ahí asigna un alloc con una zona de user buffer de tamaño 0x4100 (lo llamaremos Alloc B). Los datos asignados tendrán la siguiente forma:

Para evitar errores no deseados, previamente creé repetidamente los buffers de la misma lookasite list que Alloc B y los llené con bytes 0x0.

A continuación, el servidor SMB procederá a descomprimir los datos comprimidos y copiará los datos no comprimidos enviados por el cliente en la zona de user buffer de Alloc B:

Como podemos ver, no hay datos comprimidos porque OriginalCompressedSegmentSize = 0; el programa copiará los datos desde 0xffffd38439044060 hasta 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 de Alloc A a la zona de user buffer de Alloc B. Así, parte de la información de SRVNET_BUFFER_HDR de Alloc A se ha copiado a la zona de user buffer de Alloc B.

Ahora usaremos la técnica que mencionamos anteriormente para filtrar la dirección del allocation pool (la dirección del User Buffer).

Supongamos que queremos saber si un byte en la dirección 0xffffd3843636f15e es mayor que 0x7f o no. Procederemos a crear un paquete SMB con la siguiente información:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
¿Por qué es necesario crear un SMB así? Lo analizaremos paso a paso. Primero, la suma de OriginalCompressedSegmentSize y Offset es 0x4100, por lo que se reutilizará un alloc con un user buffer similar, es decir, el Alloc B que se usó anteriormente. Como queremos adivinar si un byte en la dirección 0xffffd3843636f15e es mayor que 0x7f, y esta dirección está a 0x210e bytes de la dirección del user buffer, los datos sin comprimir tendrán 0x210e bytes (‘B’ * 0x210e). A continuación irá la región de datos comprimidos válidos (comprimidos por la función compress()), seguida de datos comprimidos no válidos (‘\xff’ * 0x1fe9). Así, al realizar la descompresión, solo la parte de datos comprimidos válidos se descomprimirá en el otro alloc; luego la conexión se interrumpirá debido a los datos comprimidos no válidos posteriores, y la región de datos sin comprimir no se copiará en ese alloc, por lo que los datos que copiamos antes se conservarán.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

Arriba hay un Alloc que contiene la información que mencionamos, creado por el servidor SMB. A continuación, el servidor SMB llamará a la función SrvNetAllocateBuffer para crear un Alloc correspondiente. Dado que la suma de su OriginalCompressedSegmentSize y Offset es 0x4100, se reutilizará el Alloc B:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

Luego, el servidor SMB procede a descomprimir la información enviada por el cliente en el user buffer del Alloc B correspondiente al offset.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

Los datos en rojo son los datos descomprimidos; el resto se conserva. Como se puede ver en la imagen anterior, el byte que necesitamos conocer se conserva, y justo después están los datos recién descomprimidos.

Para saber si este byte es mayor que 0x7f, procedemos de la siguiente manera:

Continuamos creando un paquete SMB con el siguiente contenido:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

Aunque el total de OriginalCompressedSegmentSize y Offset es mayor que 0x4100, al asignar la región alloc para almacenar el paquete enviado desde el cliente (con un tamaño total inferior a 0x4100), el servidor SMB sigue asignando solo un Alloc cuya región de user buffer es de 0x4100 bytes, como se muestra en la siguiente figura:

Los datos resaltados en verde como los anteriores son datos del Alloc B asignado anteriormente; como pertenecen a la misma lookaside list, se reutilizan.

A continuación, el servidor SMB llamará a la función SrvNetAllocateBuffer para asignar un Alloc que contenga los datos después de la descompresión:

Mediante la descompresión, el servidor SMB tomará los datos desde User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Los datos extraídos tendrán la siguiente forma:

Con el algoritmo de descompresión mencionado anteriormente, procederá a tomar los primeros 2 bytes y usarlos como length del bloque; basándose en ese length, tomará la parte siguiente después de length y la descomprimirá. Como se indicó antes, length será 0xB0D3; sin embargo, según el algoritmo, su length real seguirá la fórmula: length = length & 0xFFF + 1 → length será 0xD4. Tomará los siguientes D4 bytes y realizará la descompresión normal hasta encontrar el byte FF (ya que los 0xD4 bytes incluyen todos los bytes 00 y una parte de los bytes FF), en ese momento los datos comprimidos se consideran no válidos, no se descomprimirá más y se cortará la conexión.

Basándonos en que el servidor corta la conexión, podemos deducir que el byte que necesitamos conocer será mayor que 0x7f.

Entonces, ¿qué ocurre si el byte que necesitamos adivinar es menor? Continuaremos con el análisis anterior, pero esta vez usaremos D7 como byte de comparación, de modo que D3 será menor que D7. Veamos qué sucederá:

Primero, enviamos al servidor SMB el siguiente paquete:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
En el lado del servidor SMB se creará un alloc de almacenamiento de la siguiente manera:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

A continuación, llamará a la función SrvNetAllocateBuffer para asignar un alloc como sigue:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

Por supuesto, este alloc se reutiliza del alloc que tiene la misma lookaside list (Alloc B). Luego el programa procede a descomprimir normalmente con los datos comprimidos válidos, y desconecta con los datos comprimidos no válidos:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

El siguiente paso es enviar de nuevo unos datos similares a los anteriores y el servidor SMB asignará un alloc correspondiente:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

Los datos marcados en verde como arriba son datos del Alloc B asignado la vez anterior, y debido a que pertenecen a la misma lookaside list, se reutilizan.

A continuación, el servidor SMB llamará a la función SrvNetAllocateBuffer para asignar un Alloc que contenga los datos después de la descompresión:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

Mediante la descompresión, el servidor SMB tomará los datos desde User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Los datos extraídos tendrán la siguiente forma:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

Igual que la vez anterior, la longitud inicial será 0xB0D3, y tras el cálculo pasará a ser 0xD4. Entonces tomará los siguientes D4 bytes y procederá a descomprimir normalmente. Sin embargo, como D4 < D7, los datos comprimidos extraídos para descomprimir en este momento contienen únicamente bytes 0. Descomprimirá normalmente hasta el final de ese bloque. A continuación, tomará la longitud del siguiente bloque a través de la longitud del bloque anterior; los dos bytes siguientes son D5 y D6 con valor 0x0, por lo que su longitud en este momento es 0x0, y por tanto la longitud extraída será 0x0000 → fin del proceso de descompresión → descompresión exitosa → el servidor SMB devuelve una respuesta → sabemos que el byte que necesitamos conocer es menor o igual que 0xD7.

De manera similar, repetiremos esto hasta filtrar los 6 bytes completos de una dirección. Así obtendremos la dirección del allocation pool.

Una vez que tenemos la dirección del allocation pool, procederemos a buscar la dirección base de srvnet obteniendo el puntero que apunta a la estructura SRVNET_RECV mediante un método similar al de filtrar la dirección del allocation pool.

Después de obtener las dos direcciones: allocation pool y SRVNET_RECV con los valores `0xffffd38439044000` y `0xffffd3843654ddd8` respectivamente, procedemos a filtrar la dirección base de srvnet.

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

Para leer el puntero AcceptSocket, debemos hacer lo siguiente:
1. Preparar un Alloc A desde una lookaside list de modo que la zona “User buffer” se llene con ceros. Este búfer contendrá después el puntero que vamos a leer. Aquí, el Alloc A se obtendrá del Alloc correspondiente a la dirección del allocation pool que filtramos. Por lo tanto, la zona User buffer del Alloc A comenzará en la dirección 0xffffd38439044050 mediante el uso de una misma lookaside list.
2. Preparar un Alloc B desde una lookaside list distinta para:
- El puntero pMdl1 apunta a la dirección del puntero AcceptSocket menos 0x18, (debido a que el offset de MappedSystemVa es 0x18 en la estructura MDL).
- El puntero pMdl2 apunta a la zona “User buffer” del Buffer A.
- El campo Flags se establece en 0x03.

Así, las direcciones de los dos punteros Mdl son respectivamente: mdl1_ptr: `0xffffd3843654de68`, mdl2_ptr: `0xffffd38439045250`.

Podemos sobrescribir los campos de la estructura SRVNET_BUFFER_HDR descomprimiéndolos desde un búfer más grande mediante la técnica descrita en la sección [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression).

Explicaré este paso con más detalle justo después del paso 4.

3. Cuando el Buffer B se libera, ocurrirán las siguientes operaciones:
- Los MDL flags se leerán desde el segundo MDL en el buffer A. Si el flag MDL_PARTIAL_HAS_BEEN_MAPPED está establecido, se llamará a MmUnmapLockedPages y el sistema podría bloquearse. Por eso debemos llenar el búfer con ceros en el paso 1.
- La zona “User buffer” del Alloc A se modificará y contendrá la información que necesitamos leer.
4. Leer el puntero AcceptSocket desde la zona “User buffer” del buffer A.
- Usar la técnica de filtrado de direcciones empleada anteriormente para leer el puntero AcceptSocket.

A continuación describiré con más detalle los pasos anteriores:

El paso 1 es bastante sencillo y similar a lo anterior, así que no volveré a mencionarlo.

En el paso 2, primero crearemos un paquete para enviar al servidor SMB con el siguiente contenido:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

Como podemos ver, OriginalCompressedSegmentSize contiene un valor negativo y la suma OriginalCompressedSegmentSize + Offset = 0x10100. Sin embargo, el tamaño del paquete que el cliente envía al servidor es mayor que 0x10100. Por lo tanto, el Alloc inicial creado por el servidor antes de la descompresión será mayor que el Alloc que contiene los datos después de la descompresión. El valor de OriginalCompressedSegmentSize se establece negativo aquí para que la suma de OriginalCompressedSegmentSize y Offset sea exactamente 0x10100, sin afectar la posición de los datos comprimidos, ya que esta depende de Offset. Y 0x38 es el offset del puntero Mdl1 en la estructura SRVNET_BUFFER_HDR.

Así, el servidor creará un Alloc que contiene los datos del cliente de la siguiente manera:

A continuación, llamará a la función SrvNetAllocateBuffer para asignar un alloc cuyo tamaño de la zona de User buffer es 0x10100, es decir, el Alloc B según los pasos anteriores:

Procede a descomprimir; por supuesto, solo se puede descomprimir una parte de los datos válidos:

Basándose en las imágenes anteriores, se puede ver que la zona de los 2 punteros Mdl en SRVNET_BUFFER_HDR del Alloc B ha sido modificada al valor que deseamos.

De manera similar a lo anterior, esta vez estableceremos el flag en 3 ajustando el offset del paquete enviado de la siguiente manera:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
Al final, tendrá esta forma:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

Cuando Alloc B se libera, se ejecutarán los siguientes fragmentos de código:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

Como se indicó arriba, pMdl1->MappedSystemVa (offset 0x18) contendrá el valor de pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

Antes de liberar Alloc B, SRVNET_RECV será:

Después de ejecutar las 4 primeras líneas del código anterior:

Antes de liberar Alloc a:

Después de ejecutar todo el código anterior:

Y los bytes que necesitamos leer en Alloc A son los bytes de color azul de la siguiente imagen:

Así pues, solo necesitamos usar la técnica de filtrar byte a byte descrita arriba para obtener la dirección de AcceptSocket + 0x50. Como en esta parte, será 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8.

De manera similar, haremos lo mismo para filtrar la dirección AcceptSocket→ srvnet!SrvNetWskConnDispatch.

Necesitamos preparar todo de la siguiente manera:

Después de que Alloc B se libere, todo cambiará así:

Los bytes que necesitamos conocer para obtener la dirección de AcceptSocket-> srvnet!SrvNetWskConnDispatch + 50 estarán en Alloc A; esos bytes son los que aparecen resaltados en azul en la imagen siguiente:

De este modo, AcceptSocket-> srvnet!SrvNetWskConnDispatch será 0xfffff80060e9d170; supongamos que ya conocemos su offset dentro del módulo srvnet.sys, podremos calcular la dirección base de srvnet.

En esta parte, la base de srvnet es: 0xFFFFF80060E70000‬ con el offset de srvnet!SrvNetWskConnDispatch igual a 0x2d170.

A continuación usaremos la técnica de primitiva Write-what-where de CVE-2020-0796 para escribir arbitrariamente en una región de memoria.

Primero, buscaremos la forma de filtrar la dirección base de ntoskrnl, mediante el filtrado de la dirección de la función IoSizeofWorkItem que importa srvnet. Para lograrlo, primero crearemos dos estructuras UNICODESTRING de la siguiente manera:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
Con `allocation_pool_object_ptr` es la dirección del allocation pool filtrado y `OFFSETS['srvnet!imp_IoSizeofWorkItem']` es el offset de la función IoSizeofWorkItem importada por srvnet.

Estas 2 estructuras UNICODE_STRING se guardarán en `allocation_pool_object_ptr + 0x1650` mediante la técnica Write-what-where encontrada en CVE-2020-0796. 
Primero almacenaremos la cadena unicode Destination en `allocation_pool_object_ptr + 0x1650`, procedemos a crear el paquete SMB de la siguiente manera:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

Arriba, los datos contienen un sentinel generado por la función os.urandom(2), que tendrá una longitud de 2 bytes, y estos 2 bytes nos permitirán saber si la dirección que filtramos es realmente la dirección que necesitamos filtrar, comparándolos después de que el proceso de filtrado se complete correctamente.

Si el tamaño total del paquete enviado por el cliente es mayor que 0x1100 (esto dependerá de los datos aleatorios antes de la compresión), entonces con seguridad allocation_pool_object_ptr se utilizará para contenerlo en el servidor SMB:

A continuación, el servidor SMB llamará a la función SrvNetAllocateBuffer para asignar una región de memoria para la descompresión. Pero como la suma de OriginalCompressedSegmentSize y Offset es 0x21, solo asigna una región con un búfer de usuario de tamaño 0x1100:

El desbordamiento de montón ocurre (como se explicó en CVE-2020-0796) y después de que el servidor SMB descomprime (antes de que se realice la copia de los datos no comprimidos), se tendrá:

De este modo, el puntero UserBufferPtr apunta al comienzo de allocation_pool_object_ptr + 0x1650, y cuando el proceso de copia se lleva a cabo, allocation_pool_object_ptr:

De manera similar, insertaremos otro sentinel debajo enviando el siguiente paquete:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
Así, cuando el servidor SMB recibe el paquete, asigna la región de memoria correspondiente; en este momento, la región de memoria asignada es allocation_pool_object_ptr y contendrá los siguientes datos:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

Tras el proceso de descompresión, los datos serán:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

De esta manera, hemos creado 2 cadenas Unicode y dos sentinels para validar los datos que filtramos.

A continuación, llamaremos a la función `RtlCopyUnicodeString` y le pasaremos las dos cadenas Unicode creadas anteriormente.

Para poder llamar a la función `RtlCopyUnicodeString`, primero sobrescribiremos el puntero HandlerFunctions con la dirección de la función `RtlCopyUnicodeString`; esta función es importada por el módulo srvnet y tiene un offset (según mi módulo) de 0x32288.

Así, con la técnica write-what-where, escribiremos la dirección 0xFFFFF80060E70000 + 0x32288 - 0x8 en HandlerFunctions.

Primero filtraremos un puntero SRVNET_RECV (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

Guardaremos la conexión para poder seguir enviando los paquetes siguientes.

A continuación, usaremos la técnica write-what-where para escribir en el puntero RtlCopyUnicodeString - 0x8 (la razón de - 0x8 es para que la función RtlCopyUnicodeString reemplace a la función Srv2ReceiveHandler en HandlerFunctions).

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

Después, escribiremos secuencialmente los 2 punteros de las 2 cadenas Unicode creadas anteriormente en los 2 argumentos de HandlerFunction.

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

En este momento, en la misma conexión, la función Srv2ReceiveHandler ha sido reemplazada por RtlCopyUnicodeString; por lo tanto, cuando enviemos un paquete, la función RtlCopyUnicodeString será invocada y copiará la cadena Unicode.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

Lo siguiente que debemos hacer es filtrar 10 bytes de dirección desde 0xffffd38439045670 hasta 0xffffd3843904567a (incluyendo los 2 sentinels en ambos extremos de la dirección a filtrar). Luego verificamos si los 2 bytes al principio y al final de la dirección filtrada son sentinels; si lo son, hemos filtrado correctamente (0xfffff8068152c380).

Después de filtrar la dirección nt!IoSizeofWorkItem (0xfffff8068152c380), le restaremos su offset (0x12C380) para obtener la dirección base de ntoskrnl (0xfffff80681400000).

Ten en cuenta que cada offset de cada archivo de módulo es diferente en las distintas versiones de Windows, así que asegúrate de tener el archivo de módulo correcto en la máquina objetivo.

De manera similar, una vez que tengamos la dirección base de ntoskrnl, podremos obtener MiGetPteAddress (0xBA968) y la dirección base de PTE (MiGetPteAddress + 0x13):

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

El siguiente paso es escribir el shellcode en 0xFFFFF78000000800 mediante la técnica write-what-where. Después, recalcularemos la dirección del shellcode dentro del PTE mediante la siguiente fórmula y limpiaremos el bit NX para que el shellcode pueda ejecutarse:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

Finalmente, escribiremos la dirección del shellcode en allocation_pool_object_ptr + 0x50 + 0x1600 y procederemos a llamar al shellcode reemplazando esa dirección con HandlerFunctions y pasando la dirección nt_base_ptr al shellcode.

¡A disfrutar del RCE! :))

Referencias

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

Descargar herramienta
→ Tamaño de asignación ↓Procesador lógico0x11000x21000x41000x81000x101000x201000x401000x801000x100100
Procesador 1📝📝📝📝📝📝📝📝📝
Procesador 2📝📝📝📝📝📝📝📝📝
...
Procesador n📝📝📝📝📝📝📝📝📝