
Exploit de prueba de concepto para CVE-2022-37969, una escalada de privilegios local en el controlador Windows Common Log File System. Demuestra heap spray, robo de tokens y escritura arbitraria en el kernel para obtener privilegios de SYSTEM.
autores: Ricardo Narvaja & Daniel Kazimirow (Solid)
Solo con fines de demostración. El exploit completo funciona en sistemas vulnerables Windows 11 21H2.
PoC funcional basado en información previamente publicada por Zscaler
Consulte el artículo Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation.
Recorrido de explotación:
El escenario utilizado aquí fue Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918
El primer paso es crear un archivo llamado MyLog.blf en la carpeta pública (%public%), utilizando la función CreateLogFile():



Luego crea varios archivos de registro con nombres aleatorios usando un bucle.
Y dentro del bucle, llama a nuestra función getBigPoolInfo():

Llama a NtQuerySystemInformation(), con 0x42 (66 decimal) como primer argumento, devolverá en v5 la información sobre las asignaciones realizadas en el bigpool, cuya estructura es de tipo SYSTEM_BIGPOOL_INFORMATION.

Tenemos que llamar a esta función dos veces. La primera devolverá un error, pero nos dará el tamaño correcto del búfer para llamar la segunda vez y obtener la información deseada.

v5 recibirá la información de la estructura SYSTEM_BIG_POOL_INFORMATION.

El número de asignaciones en el bigpool se almacena en el primer campo llamado Count; en el segundo campo hay un arreglo de estructuras SYSTEM_BIGPOOL_ENTRY.

Luego buscaremos en todas las estructuras la etiqueta "Clfs" y el tamaño 0x7a00.

Almacena en un arreglo llamado kernelAddrArray la VirtualAddress que es el primer campo de cada estructura que tiene etiqueta CLFS y tamaño 0x7a00. De ahora en adelante, los pools que cumplan ambas condiciones se llamarán: "pools correctos".

Además de almacenar cada pool correcto en el arreglo, almacena el último pool correcto encontrado en el contenido de la variable a2, que se utiliza como argumento de la función.

De esta manera, a2 siempre apunta al último pool correcto con etiqueta CLFS y tamaño 0x7a00 creado.
La variable v26 siempre almacena el pool correcto anterior encontrado, ya que es igual a v24 (v26=v24), antes de llamar a getBigPoolinfo(), pero v24 se actualiza al salir de esta llamada con el último pool correcto encontrado, y v26 se queda con el pool correcto anterior encontrado.

Luego resta ambas direcciones, y en caso de que el resultado sea negativo, invierte los operandos para que siempre sea positivo.

De esta manera, en v32 se almacenará la diferencia entre la VirtualAddress de los dos últimos pools correctos encontrados.
Luego hace algo similar, en este caso v23 es inicialmente cero, así que hace v23=v32 la primera vez.

La siguiente vez en el bucle v23 todavía tiene el mismo valor y no es cero, por lo que se rompe y va aquí.

V32 tiene la última diferencia y v23 la anterior; si son iguales, sale e incrementa uno, pero reinicia el contador a cero.
La idea es encontrar 6 comparaciones consecutivas de etiquetas CLFS y tamaño 0x7a00 cuyas diferencias sean iguales, y esa diferencia será 0x11000. Veremos al ejecutar que cuando encuentre 6 (ya que empieza desde cero) consecutivas con distancias iguales, dará ese valor de diferencia entre ellas.


Allí vemos que encontró 6 consecutivas y salió del bucle de creación de archivos de registro.
En la carpeta "public" podemos ver los archivos creados.

Nuestra función craftFile() abre el archivo original (MyLog.blf) y lo modifica para desencadenar el error.

Después de modificar el archivo, es necesario cambiar el CRC32, de lo contrario obtendremos un error de archivo corrupto.
Este valor se encuentra en el offset 0x80C del archivo.

A continuación, realiza un HeapSpray, utilizando la función VirtualAlloc() para asignar memoria, en direcciones arbitrarias 0x10000 y 0x5000000 respectivamente, y guardando en la segunda asignación (0x10000), el valor 0x5000000, cada 0x10 bytes.

Usa CreatePipe() para crear un pipe anónimo y llama a NtFsControlFile() usando 0x11003c como argumento para agregar un atributo; luego se puede llamar a esta misma función con el argumento 0x110038 para leerlo.
Más detalles de este método se pueden encontrar AQUÍ

Allí vemos el búfer de entrada que es el atributo que estamos agregando; si llamamos a NtFsControlFile() nuevamente con el argumento 0x11038, en la salida debería devolver este mismo atributo.

Busca en el pool la etiqueta del atributo creado (NpAt)


Y cuando lo encuentra, guarda en v30.Pointer la VirtualAddress de este pool.
V30.pointer+24 apunta a AttributeValueSize en el pool del kernel y lo guarda en uno de los HeapSprays que hicimos antes.

La idea es escribir en esa dirección del kernel+8, para sobrescribir el AttributeValue.


La estructura PipeAttribute tiene como primer campo una LIST_ENTRY de 16 bytes, luego un puntero al nombre del atributo de 8 bytes y luego, en 0x18 (24 decimal), el campo AttributeValueSize que es el que estamos almacenando en el HeapSpray.
Después de eso, cargamos CLFS.sys y ntoskrnl en modo usuario, y usando GetProcAddress() encontramos las direcciones de las funciones ClfsEarlierLsn() y SeSetAccessStateGenericMapping().

Luego llamamos a la función FindKernelModulesBase() que encontrará la base del kernel de ambos módulos usando NtquerySystemInformation() esta vez con el argumento SystemModuleInformation para devolver la información sobre todos los módulos.

De esta manera, podemos calcular el offset de cada función y luego obtenerlas en el kernel.

La función pipeArbitraryWrite() se llama dos veces; hay una bandera que inicialmente es cero para la primera llamada y cuando en la segunda llamada su valor es 1, cambiará los valores del HeapSpray.

En la primera llamada, en la dirección de memoria 0x5000000 se ubican los siguientes valores.

Recuerde que este valor, además de asignarse en esa dirección, se almacena en nuestro HeapSpray.

Así es como queda la memoria después de la primera llamada, como dijimos, en la dirección alrededor de 0x5000000.

Y en el HeapSpray, desde la memoria 0x10000, almacenará el puntero a AttributeValueSize cada 0x10 bytes, además del puntero a 0x5000000.

Esta secuencia desencadenará el error:

Se llama nuevamente a CreateLogFile() en el archivo manipulado y en otro con un nombre aleatorio.
Luego se llama a AddLogContainer() usando los handles de esos archivos.

Se llama a NtSetinformationFile() y se cierran los handles con los que se corrompe el puntero (se explicará más adelante).

El HeapSpray evita que ocurra un BSOD en este punto:

Al establecer un breakpoint allí, podemos ver que el puntero está corrupto y apunta a nuestro HeapSpray, con lo cual podemos manejar las siguientes dos llamadas a funciones de la vtable.


RAX toma el valor 0x5000000 y salta primero a la función ubicada en 0x5000000+18 y luego a 0x5000000+8.


Así que primero salta a fnClfsEarlierLsn() y luego a fnSeSetAccessStateGenericMapping().
Rastreamos desde el breakpoint y vemos que llega a CLFS!ClfsEarlierLsn().

Esta función se llama exclusivamente porque cuando retorna, establece EDX en 0xFFFFFFFF.

En la dirección 0xFFFFFFFF habíamos almacenado el resultado de SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Como mencionamos, al retornar de CLFS!ClfsEarlierLsn(), el valor de RDX es 0x00000000FFFFFFFF.

Llegamos a la segunda función nt!SeSetAccessStateGenericMapping().

Esta función es útil, ya que RCX apunta a nuestro HeapSpray, y RDX vale 0xFFFFFFFF, cuyo contenido controlamos.


El contenido de RCX+0x48 tiene el puntero a AttributeValueSize que se almacenó en v30.Pointer+24.



Ese valor del puntero de AttributeValueSize se mueve a RAX, luego lee el contenido de la dirección 0xFFFFFFFF donde habíamos almacenado la dirección de SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Luego sobrescribe en RAX+8 el siguiente campo que es el AttributeValue().


Por supuesto, el AttributeValue normalmente apuntaría en el kernel al atributo que agregamos.

Y ahora lo sobrescribiremos con un puntero al resultado de SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF00.
Eso significará que cuando llamemos nuevamente a la función NtFsControlFile(), esta vez con el argumento 0x110038 para leer el atributo, en lugar de devolver la "A" a la que apuntaba el puntero AttributeValue, ahora leerá desde _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 la cantidad de bytes solicitada y la devolverá en el búfer de salida, con lo cual podemos obtener en la primera llamada el valor del SYSTEM TOKEN.

v9b es la dirección de inicio del Output Buffer donde se copió el contenido del resultado de SYSTEM EPROCESS & 0xFFFFFFFFFFFFFFF000.
A eso le suma v14, que son los últimos 3 bytes de SYSTEM EPROCESS, y luego suma 0x4b8, que es el offset de Token para esta versión de Windows 11; luego encuentra el contenido de esa dirección que tendrá almacenado el valor del System Token.



Recuerde que los últimos 4 bits se cambiaron, no es significativo, por lo que el valor aún coincide.
En la segunda llamada, el valor de Flag es 1 ya que se incrementó al final de la primera llamada.

Allí vemos el orden en que se almacenan los valores.

La dirección 0xFFFFFFFF con el valor que acabamos de encontrar del System Process Token.


Y en el HeapSpray está el valor de la dirección del Token de mi proceso al que le resto 8. Ese valor más ocho se usará como objetivo, recuerde que se escribe en la dirección apuntada por RAX+8.


En la dirección de memoria que comienza en 0x5000000.

También vemos que usa el nombre de otro contenedor, ya que el anterior está siendo usado por el proceso del sistema y no se puede abrir ni eliminar nuevamente.

Luego, el error se desencadena por segunda vez de la misma manera que en el primer intento.

Vuelve a CLFS!ClfsEarlierLsn().

estableciendo RDX en 0xFFFFFFFF.

Luego va a nt!SeSetAccessStateGenericMapping().

Lee la dirección del Token de mi proceso menos 8, donde va a escribir.

Luego lee el SYSTEM TOKEN.

Y escribe en la dirección del Token de mi proceso (suma 8), el System Token.

Y de esta manera, mi proceso tiene el System Token.

Una vez que se escribe el token, iniciamos un proceso para verificar los privilegios; en este caso, lanzamos Notepad.exe.



Recuerde que este PoC solo funciona en Windows 11; en Windows 10 producirá un BSOD, por lo que deberá hacer algunas modificaciones para que funcione correctamente. Esto no se explica en esta publicación de blog.
Análisis de las estructuras
Las estructuras y la mayor parte de la documentación sobre el formato de archivo CLFS las hemos tomado del excelente trabajo de IONESCU sobre CLFS Internals.
Podemos ver que se ha agregado una comprobación en la función ClfsBaseFilePersisted::LoadContainerQ.

Los valores que realizan una suma pertenecen a la estructura _CLFS_BASE_RECORD_HEADER.

Tenga en cuenta que el Base Block comienza en el offset 0x800 del archivo y termina en el offset 0x71FF, correspondiendo los primeros 0x70 bytes al Log Block Header.
Como buena práctica, podemos añadir la estructura _CLF_LOG_BLOCK_HEADER en IDA
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
Luego tenemos el Base Record Header (_CLFS_BASE_RECORD_HEADER) que comienza en el offset 0x870 desde el inicio del archivo y tiene una longitud de 0x1338 bytes.

Si quieres importarlo a IDA, antes debes agregar los siguientes tipos y estructuras faltantes
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
Ahora está listo para ser añadido:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

Después de incluir las estructuras, notamos que realiza una suma entre el cbSymbolZone y la dirección donde termina el _CLFS_BASE_RECORD_HEADER. (inicio + 1338h)
Recuerda que cbSymbolZone se modificó en el archivo de registro manipulado de 0x000000F8 a 0x0001114B.
(offset 0x1b98 del archivo)
0x800(offset del inicio del Base Block) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
cbSymbolZone manipulado en el archivo MyLog.blf:


Como el parche está en la función CClfsBaseFilePersisted::LoadContainerQ, tenemos que echar un vistazo al objeto CClfsBaseFilePersisted.
Estableciendo un punto de interrupción en CLFS!CClfsBaseFilePersisted::LoadContainerQ y cuando se llama a CreateLogFile con el identificador del archivo manipulado, se detendrá.

Llama a la función CClfsBaseFile::GetBaseLogRecord para obtener la dirección del Base Log Record (_CLFS_BASE_RECORD_HEADER)

RAX apuntará a la dirección de _CLFS_BASE_RECORD_HEADER

Observa la estructura _CLFS_BASE_RECORD_HEADER en memoria y el campo cbSymbolZone 0x1328
bytes adelante


r14 almacena la estructura correspondiente al “this”, que es CClfsBaseFilePersisted ya que es el this de la función CClfsBaseFilePersisted::LoadContainerQ.

La estructura CClfsBaseFilePersisted en memoria:

Entonces, creemos una estructura con una longitud de 0x21c0 para completar sus campos mientras la invertimos (es una estructura no documentada) la llamaremos struct_CClfsBaseFilePersisted

Dentro de la función CClfsBaseFile::GetBaseLogRecord() obtiene el puntero a _CLFS_BASE_RECORD_HEADER. y sabemos que el "this" en esa función es la estructura: struct_CClfsBaseFilePersisted.

Lee dos campos (offset 0x28 y 0x30)

El campo 0x28 es una palabra y tiene el valor 6, así que cambiamos el tipo a word en la estructura.



Por ahora, lo renombramos como constante 6 (const_6)


Según la documentación, 6 sería el número de bloques CLFS_METADATA_BLOCK_COUNT. El campo podría referirse a este valor.
Y ese puntero está en el offset 0x30.

Observa que el tamaño mostrado allí incluye el encabezado con una longitud de 0x10


Cuando se llama a la función ExAllocatePoolWithTag, se solicitan unos pocos bytes, pero el encabezado no está incluido, por lo tanto, se solicitarán 0x90 bytes (0xa0 – 0x10) en la llamada.
Buscando por texto +30h], las instrucciones que escriben en el offset 0x30 encontramos una larga lista, pero filtrando la lista por el tipo de objeto CClfsBaseFilePersisted nos quedamos con pocos resultados e inmediatamente encontramos dónde se asigna ese tamaño, y la misma etiqueta. (Consejo: los nombres de funciones Create e Initialize son siempre los primeros a buscar)


Como aún no conocemos el nombre, lo pondremos pool_0x90, que es otra estructura no documentada, y crearemos una estructura de ese tamaño.


El pool_0x90 en memoria tiene otro puntero en su propio offset 0x30.

Este otro puntero apunta al bloque base en el archivo (el bloque base comienza en el offset 0x800)


Imagen tomada del blogpost de Zscaler:

La asignación es enorme, porque contiene todo el bloque base.



Entonces, crearemos una nueva estructura de tamaño 0x7a00 y la llamaremos BASE_BLOCK

Los primeros 70 bytes ya sabemos que corresponden a _CLFS_LOG_BLOCK_HEADER y los siguientes 0x1338 a _CLFS_BASE_RECORD_HEADER.

Entonces, sumando el inicio del Base Block con el offset al siguiente registro (que es 0x70), obtenemos el _CLFS_BASE_RECORD_HEADER

El _CLFS_BASE_RECORD_HEADER en memoria.

Mirando otros métodos del mismo objeto CClfsBaseFilePersisted, en CClfsBaseFilePersisted::AddContainer se obtiene con CClfsBaseFile::GetBaseLogRecord también la dirección de _CLFS_BASE_RECORD_HEADER.

A continuación, llama a CClfsBaseFile::OffsetToAddr usando cbOffset, obtiene la dirección de _CLFS_CONTAINER_CONTEXT, y almacena cboffset en el array rgbcontainers que está en el offset 0x328 de _CLFS_BASE_RECORD_HEADER.

La función CClfsBaseFile::OffsetToAddr se utiliza para encontrar direcciones de estructuras a partir de un offset

En este punto, el offset del contenedor que se almacenará en 0x328 sigue siendo 0, porque aún no hemos agregado un contenedor.

el PoC llama a CreateLogFile dos veces, la primera vez con el archivo malformado MyLog.blf y la segunda vez con el archivo normal MyLogxxx.blf, por lo que debemos detener la depuración dos veces en todos los lugares anteriores y tomar nota en un bloc de notas de las direcciones de las estructuras anteriores para ambos archivos.

Avancemos un poco hasta CLFS!CClfsLogFcbPhysical::AllocContainer estableciendo un punto de interrupción allí y ejecutando hasta allí.
Cuando se alcanza AddLogContainer() en el POC, nos detenemos en el punto de interrupción.

Establezcamos también un punto de interrupción en CClfsBaseFilePersisted::AddContainer+176 donde vimos antes que encontrará el offset y el puntero a la estructura _CLFS_CONTAINER_CONTEXT.


Cuando el depurador se detiene, podemos ver que el offset es 0x1468.

En RAX se devolverá la dirección de la estructura _CLFS_CONTAINER_CONTEXT.

la estructura todavía está vacía porque aún no se ha agregado el contenedor.

Observa que el valor SignatureOffset=0x50 que escribimos en el offset 0x868 del archivo malformado, restando el 0x800 del inicio del bloque base, estará en la estructura _CLFS_LOG_BLOCK_HEADER en el offset 0x68.


Cuando el PoC llama a la función AddLogContainer() usando el archivo malformado, en el offset 0x68 de _CLFS_LOG_BLOCK_HEADER, en lugar del valor 0x50 que escribimos allí, actualmente hay un 0xFFFF0050 en memoria.

En algún momento, ese valor fue alterado por el programa; para ver cuándo ocurrió, en la siguiente ejecución estableceremos un punto de interrupción de memoria en escritura.
El offset se almacena en r15 + 0x328 (r15 apunta a la estructura _CLFS_BASE_RECORD_HEADER)


RBX almacena el offset 0x1468.

Entonces, en la dirección del Base Block + 0x70 + el offset 0x1468 que encontramos, estará la dirección del contenedor CLFS_CONTAINER_CONTEXT.

En la estructura CLFS_CONTAINER_CONTEXT en el offset 0x18 estará el puntero pContainer que se almacenará allí; podemos establecer un punto de interrupción en escritura y ver cuándo se escribe.


Este es el puntero que debemos corromper, ya que en la función donde está la vulnerabilidad, primero lee el CLFS_CONTAINER_CONTEXT, luego lo mueve a r15 y a continuación lee el valor de r15+18, que es este puntero que acabamos de establecer el punto de interrupción en escritura.


almacena el pContainer en el offset 0x1c0 de la estructura struct_CClfsBaseFilePersisted.

Después de varias veces que se detiene, llegamos al momento en que se corrompe. La parte superior de la dirección del puntero ha cambiado de FFs a cero.

Esto sucede cuando se llama al segundo AddLogContainer() del archivo malformado, el puntero del MyLogxxx anterior se corrompe.
El problema ocurre porque el SignaturesOffset, que debería ser 0x50, ahora es 0xFFFF0050, lo que permite escribir fuera de los límites en el memset que sigue.


La función memset() va a corromper la estructura _CLFS_CONTAINER_CONTEXT que está debajo; esta estructura corresponde al archivo MyLogxxx, ya que cuando se crearon, se ubicaron a 0x11000 bytes de distancia entre sí.
De esta manera, calcula exactamente dónde escribir en la siguiente estructura y pone a cero la parte superior del puntero, por lo que apunta al heap del usuario donde se creó el HeapSpray.
La estructura del bloque base del archivo malformado está justo 0x11000 antes que la del archivo MyLogxxx.
Malformado:

MyLogxxx


RCX es más pequeño que RDX ya que se sumó 0xFFFF0050, en lugar de 0x50 como debería ser.

y llegamos a la función memset(), para establecer la cantidad de 0xb0 bytes con ceros, con RCX apuntando a la estructura CLFS_CONTAINER_CONTEXT del archivo MyLogxxx, específicamente a los cinco bytes altos de pContainer.

Este puntero será corrompido sobrescribiendo los primeros bytes:

quedando apuntando a una dirección de memoria previamente controlada por nosotros mediante HeapSpray


Luego, se cerrará el identificador del archivo MyLogxxx, y se llega a CClfsBaseFilePersisted::RemoveContainer, la vulnerabilidad finalmente se desencadena.

Ahora que tenemos más información, notamos que aquí lee el Base_Block.LOG_BLOCK_HEADER.SignaturesOffset y el Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount
En la primera parte del parche, ese SignaturesOffset no debe ser mayor que 0x7a00; en el nuestro originalmente era 0x50, si llegaba con un valor mayor que 0x7a00 nos expulsaría.

Ejecutando el PoC en la máquina parcheada, compara 0x50 con 0x7a00 y como es menor continúa.

En el siguiente bloque, se suma el cbSymbolZone malformado al valor de la dirección final de _CLFS_BASE_RECORD_HEADER y esta suma se almacena en result_1.

Luego, se suma la dirección del Base_Block con el valor de SignatureOffset, que en un archivo normal es 0x7980.

La dirección máxima del base_block es 0x7a00, ahora la SymbolZone está permitida hasta 0x80 antes del límite.
Lo almacenará en result_2, es decir, ese sería el límite máximo para la SymbolZone dentro del base block, luego compara ambos resultados; si el primero es mayor que el segundo, significa que se salió de los límites.


Obviamente el primer miembro será mayor que el segundo y no continuará, ya que la primera suma de cbSymbolZone + dirección final de _CLFS_BASE_RECORD_HEADER excede el límite (que es result_2) y conduce a un "fuera de límites".

Lo último que tendríamos que averiguar es dónde el valor SignatureOffset de 0x50 se convierte en 0xFFFF0050.Así que, empecemos de nuevo, reiniciemos y paremos en CLFS!CClfsBaseFilePersisted::LoadContainerQ donde el valor aún no ha sido modificado en memoria y sigue siendo 0x50.
Establezca un punto de interrupción de acceso en el desplazamiento 0x68 en SignatureOffset.

Y después de varias paradas, detectamos el momento adecuado cuando modifica el valor, en ClfsEncodeBlockPrivate.

Esta función no está parcheada, por lo que podría ser un comportamiento causado por el bajo valor de 0x50 y el resto de valores siendo manipulados.
Entre los valores manipulados, podemos ver el valor ccoffsetArray cuyo nombre en la estructura _CLFS_BASE_RECORD_HEADER es rgClients y representa el array de desplazamientos que apuntan al objeto Client Context.
El campo rgClients se encuentra en el desplazamiento 0x138 (0x9a8-0x800-0x70) de la estructura _CLFS_BASE_RECORD_HEADER.


En el PoC, este valor está malformado para apuntar a un objeto de contexto de cliente falso, llamado FakeClientContext

Esta es la estructura Client Context _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
El valor eState está en el desplazamiento 0x78 desde el inicio de la estructura, en el archivo manipulado 0x23a0+0x78.


Este valor muestra el estado del registro.
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
este valor se establece en CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20
El otro valor malformado es fAttributes que corresponde al conjunto de banderas FILE_ATTRIBUTE asociadas con el archivo de registro base (como System y Hidden).


Dado que el campo comienza un byte antes en 0xa y abarca dos bytes, el valor de fAttributes es 0x100.


Finalmente, está el valor blocknameoffset que apunta al desplazamiento 0x1bb8, es decir, sumando 0x78 y 0x800 apunta al desplazamiento 0x2428 del archivo.


Nótese que el desplazamiento hacia el Client Context es 0x1b30

Por lo tanto, el Client Context está en el desplazamiento 0x23a0.


Y justo 0x10 antes, está el valor correspondiente a blocknameoffset.


Que apuntaría a la cadena con el nombre
el último es blockattributeoffset que está 0xC antes del Client Context en 0x2394.

Estos dos últimos valores pertenecen a una estructura anterior al Client Context de 0x30 bytes de longitud, llamada _CLFSHASHSYM
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


están a 0x20 y 0x24 bytes del inicio de la estructura _CLFSHASHSYM, por lo que en la estructura _CLFSHASHSYM el valor llamado blockNameOffset en el POC es el campo cbSymName y el blockAttributteoffset es el campo cbOffset.


Esos son los valores malformados, ahora necesitamos ver cómo afectan para cambiar nuestro SignaturesOffset del valor 0x50 a 0xFFFF0050.
Echemos un vistazo a la función CClfsBaseFile::AcquireClientContext(), que debería devolver el contexto del cliente.

llama a CClfsBaseFile::GetSymbol con el cuarto argumento que será _CLFS_CLIENT_CONTEXT ** donde almacenará el puntero al Client Context.

Dentro de la función CClfsBaseFile::GetSymbol pasamos el desplazamiento ccoffsetArray malformado a CClfsBaseFile::OffsetToAddr y obtenemos la dirección del contexto del cliente, establezcamos un punto de interrupción allí para que se detenga al llamar al archivo creado con CreatelogFile .

Ahí está detenido con el argumento manipulado ccoffsetArray.


La función CClfsBaseFile::OffsetToAddr devuelve el falso Client Context

Y comprueba que el valor de cbOffset no sea cero ya que 0xC se encuentra antes de la estructura _CLFS_CLIENT_CONTEXT que está en RAX.


Luego compara cbOffset con ccoffsetArray (que está en RSI), deben ser iguales, de lo contrario obtendremos un error.

También comprueba que cbSymName sea igual a cbOffset+0x88, si no, también obtendremos un error.

Y finalmente, compara el byte cidClient con cero

Si todas esas comprobaciones son exitosas, se guardará el client context.

La salida de la función r14 apunta al Client Context

Al salir de CClfsLogFcbPhysical::Initialize tendremos la dirección de CLFS_CLIENT_CONTEXT.

Ahora lee el valor de fAttributes (0x100)

esta función pertenece a la clase CClfsLogFcbPhysical


Que fue asignada aquí, y su tamaño es 0x15d0 y su etiqueta es “ClfC”

Vamos a crear una estructura para almacenar lo que estamos invirtiendo, la llamaremos: struct_CClfsLogFcbPhysical.

Nótese que en 0x2b0 guarda la dirección de la estructura CClfsBaseFilePersisted.

Después de guardar muchos valores en la estructura, va a una parte importante, prueba el eState con 0x20.


Dado que el valor manipulado era 0x20, la prueba devolverá 1.


Vemos que en el constructor en la vtable está

Comprobará si el archivo es multiplexed.

Entonces, va por el camino deseado, alcanzando CClfsLogFcbPhysical::ResetLog.


Varios campos se inicializan a cero excepto uno que se inicializa a 0xFFFFFFFF00000000.

Aquí recupera el Client Context

almacena el valor 0xFFFFFFFF00000000.



Escribe 0xFFFFFFFF en el desplazamiento 0x5c que es la parte alta de CLFS_LSN lsnRestart.ullOffset



Ahora ejecutamos la función ClfsEncodeBlockPrivate(), que es la responsable de sobrescribir el 0x50 con 0xFFFF0050 como hemos visto antes.
Ahí lee el valor de SignatureOffset = 0x50 que sigue siendo como lo pusimos en el archivo malformado y lo suma al inicio de CLFS_LOG_BLOCK_HEADER.

esto es un bucle que escribe 2 bytes, como el SignatureOffset en lugar de apuntar a un valor correcto que en un archivo normal es un valor alto, por ejemplo 0x3f8 que hace que escriba más adelante, aquí escribirá en el mismo CLFS_LOG_BLOCK_HEADER
La idea es cambiar el destino de escritura para intentar corromper el valor SignatureOffset.
Archivo Normal

En este punto, comenzará a iterar y escribir dos bytes.

El contador debe alcanzar el valor 0x3d para salir del bucle.

RCX está aumentando desde 0x200, ya estamos en el tercer ciclo, y su valor es 0x600

en la iteración 0xe, RCX es 0x1a00


Ese fue donde había escrito el 0xFFFFFFFF000000.


Está leyendo los últimos dos bytes FFFF

Y los copiará en R8


Como hemos visto, este valor es crítico ya que permite omitir la comprobación y escribir fuera de los límites para corromper el puntero pContainer del archivo que sigue al memset() y escribir ceros en la parte superior y dejarlo apuntando a nuestra memoria controlada (HeapSpray).
En CClfsBaseFilePersisted::AllocSymbol que la misma suma que va a obtener el destino del memset que es cbSymbolZone + dirección final de CLFS_BASE_RECORD_HEADER lo compara antes contra Base_block + 0xFFFF0050, por lo que tiene valores corrompidos en ambos lados de la ecuación.
CbSymbolZone= 0x1114B
Es el valor malformado que sumado a la dirección final de CLFS_BASE_RECORD_HEADER hará que escriba fuera de los límites y el otro miembro de la comparación que debería ser la dirección del Base Block + SignatureOffset, permanece SignatureOffset =0xFFFF0050 lo que permite que esta comprobación pase y escriba fuera de los límites en el memset() y ponga a cero la parte superior del puntero que permanecerá apuntando a nuestro HeapSpray.

Dado que RCX es menor que RDX.

Como hemos visto antes. (Los valores pueden diferir porque pertenecen a una ejecución anterior)
Corromperá el puntero, estableciendo los bytes más altos a 0

Dejándolo apuntando a un área de memoria que controlamos mediante HeapSpray


Entonces, cuando se activa la vulnerabilidad, llegamos a CClfsBaseFilePersisted::RemoveContainer

Allí estará el puntero ya corrompido y se puede explotar como vimos anteriormente.

En este punto tenemos el error explotado, lleva a controlar las funciones que permiten leer el token SYSTEM y escribir en nuestro propio proceso para lograr la escalada de privilegios local.
Esperamos que les sea útil, si tienen alguna duda pueden contactarnos en [email protected] y [email protected]
¡Disfruten!