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
Herramientas/GitHubGitHub/fortra/cve-2023-28252
Escalada de PrivilegiosForensia de MemoriaAnálisis de VulnerabilidadesExplotaciónIngeniería InversaDepuradoresExplotación de Binarios
GitHubfortra/cve-2023-28252

CVE-2023-28252

Análisis técnico y exploit de prueba de concepto para CVE-2023-28252, una vulnerabilidad de escalada de privilegios en el controlador del Sistema de Archivos de Registro Común (CLFS) de Windows utilizada en ataques del ransomware Nokoyawa.

Ver Repositorio
18444hace 3 añosRevisado por Kitploit

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

Desde febrero de 2022 se reportó un nuevo ransomware que parece estar utilizando una vulnerabilidad de día cero de Windows, según la investigación realizada por Trend Micro.
Se puede encontrar más información sobre este ransomware en este enlace.
Según el análisis de Kaspersky, el grupo de ransomware Nokoyawa ha utilizado otros exploits dirigidos al controlador Common Log File System (CLFS) desde junio de 2022, con características similares pero distintas, todas vinculadas a un único desarrollador de exploits.
En abril de 2023, cuando Microsoft lanzó el parche, se asignó CVE-2023-28252.
Anteriormente, en 2022, investigamos un error similar en el mismo componente y lo documentamos en este blogpost

Formato de archivo del Common Log File System (CLFS):

Para abordar el análisis, es necesario conocer el formato de archivo .blf, que es manejado por el controlador vulnerable Common Log File System llamado CLFS.sys y que se encuentra en la carpeta de controladores dentro de system32.

Se puede encontrar más información sobre este tipo de archivo en los siguientes enlaces:

https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-the-common-log-file-system

https://github.com/ionescu007/clfs-docs/blob/main/README.md

https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe

La vulnerabilidad:

Este análisis se realiza para Windows 11 21H2, clfs.sys versión 10.0.22000.1574, aunque también funciona en Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 y Windows Server 2022.

En versiones anteriores de Windows, es necesario ajustar algunos valores, de lo contrario se produciría un BSOD.

Microsoft Patch Tuesday de abril de 2023.

Una captura de pantalla de una computadora, descripción generada automáticamente con confianza mediaPuede verificar la versión del controlador como se muestra

Cuando se publicó la vulnerabilidad, en abril de 2023 empecé con Esteban Kazimirow a realizar la ingeniería inversa del controlador CLFS.sys, aunque en este caso, solo analizar el parche era muy difícil de deducir dónde estaba el error y cómo desencadenarlo, ya que la explotación es muy compleja.

Más tarde, apareció un blogpost cuyo autor, a partir de una muestra de un malware, mostró algunas partes del código descompilado por HexRays y algo de información que orientaba hacia dónde debía enfocarse la explotación.

Obviamente, la información proporcionada no era completa, pero sin esta ayuda habría sido poco probable haber llegado a construir el PoC y posteriormente un exploit funcional.

Para facilitar su comprensión, primero explicaremos cómo construir el PoC y luego haremos el análisis de la vulnerabilidad.

Este blogpost contiene dos secciones:

Construcción del PoC:

1- Obtener las direcciones del kernel que necesitamos para la explotación

2- Preparar la ruta para crear los archivos .blf:

3- Crear el archivo "trigger blf" usando la función CreateLogFile()

4- Modificar el archivo "trigger blf"

5- Obtener la dirección en kernel del BASE BLOCK del trigger blf

6- Llamar a AddLogContainer con el manejador del trigger blf

7- Preparar los archivos spray blf

8- Preparar la memoria para realizar el spray

9- Desencadenar el error

Depuración:

1- Verificar el spray de memoria

2- Observar el RecordOffset[12] del trigger blf

3- Observar el valor iFlushBlock en el archivo spray blf

4- ¿Por qué lee desde BLOCK 1 SHADOW en lugar de BLOCK 0 CONTROL?

5- ¿Por qué la suma de verificación es igual a cero en los archivos spray blf?

6- Finalizar la explotación.

7- El parche real

Construcción del PoC:

1- Obtener las direcciones del kernel que necesitamos para la explotación

Crearé una función llamada InitEnvironment para obtener algunas direcciones necesarias del kernel.

Obtendré la dirección EPROCESS de mi proceso y la almacenaré en la variable g_EProcessAddress, luego la dirección EPROCESS del proceso SYSTEM y la almacenaré en system_EPROCESS, luego la dirección EHTREAD del hilo principal de mi proceso y la almacenaré en g_EThreadAddress y finalmente la dirección del PREVIOUS MODE que en esta versión del PoC no se usará.

Una captura de pantalla de código de computadora, descripción generada automáticamente con baja confianza

Este método es bien conocido, la función GetObjectKernelAddress llama a NtQuerySystemInformation dos veces con el primer argumento SystemExtendedHandleInformation, la primera llamada se realiza con un tamaño incorrecto y devuelve un error, pero también devuelve el tamaño correcto que se usa en la segunda llamada y obtiene la información de todos los manejadores, luego recorre en un bucle la información de cada manejador y en el campo Object del handleinfo correcto obtiene la dirección buscada en el kernel.

Una imagen que contiene texto, captura de pantalla, fuente, línea, descripción generada automáticamente

También necesito las direcciones en kernel de las siguientes funciones exportadas por CLFS.sys:

• ClfsEarlierLsn

• ClfsMgmtDeregisterManagedClient

Y las funciones exportadas desde NTOSKRNL.exe

• RtlClearBit/PoFxProcessorNotification

• SeSetAccessStateGenericMapping

Para obtener estas direcciones se utiliza un método similar al que se usa para obtener la base en kernel de ambos módulos, llamando a NtQuerySystemInformation dos veces, pero en este caso el primer argumento será SYSTEM_INFORMATION_CLASS (en el PoC usamos la función FindKernelModulesBase para este propósito).

Una imagen que contiene texto, fuente, captura de pantalla, línea, descripción generada automáticamente

Luego carga CLFS.sys y NTOSKRNL.exe como módulos normales en modo usuario llamando a LoadLibrary, obtiene las direcciones en modo usuario con GetProcAddress y luego resta la imagen base de cada uno, lo que da el desplazamiento de la función y finalmente suma cada desplazamiento a las bases correspondientes del kernel y así obtiene las direcciones en kernel de todas las funciones necesarias.

Una imagen que contiene texto, fuente, línea, captura de pantalla, descripción generada automáticamente

2- Preparar la ruta para crear los archivos .blf:

Creo una función llamada createInitialTriggerBlfFile que generará y escribirá un archivo .blf.

La ruta que se utiliza como argumento en CreateLogFile es diferente de una ruta normal, por ejemplo, para abrir el archivo 1280.blf ubicado en la carpeta C:\Users\Public, debemos establecer la ruta LOG:C:\Users\Public\1280. Esto se guardará en la variable stored_name_CreateLog.

Hago esto usando wsprintfW() ya que stored_env almacena la ruta C:\Users\Public, obtenida previamente de las variables de entorno. A esta cadena le antepondré la cadena LOG: y un nombre aleatorio al final, sin la extensión .blf.

Una captura de pantalla de una computadora, descripción generada automáticamente con confianza media

Esta será la ruta a mi archivo inicial que llamaré "trigger blf". Por supuesto, también debo guardar la ruta normal al mismo archivo sin el LOG: al inicio y con la extensión BLF para abrirlo y modificarlo con CreateFile(), WriteFIle() como cualquier otro archivo; esta ruta será, por ejemplo: C:\Users\Public\1280.blf, y se almacenará en la variable stored_name_fopen.

Una imagen que contiene texto, fuente, línea, captura de pantalla, descripción generada automáticamente

Por supuesto, ambas rutas corresponden al mismo archivo, y debo usar una u otra según corresponda.

3- Crear el archivo "trigger blf" usando la función CreateLogFile().

La función CreateLogFile cumple una función bastante similar a CreateFile() (crea archivos nuevos o abre archivos existentes y obtiene su manejador), incluso algunos argumentos son similares, pero CreateLogFile() solo funciona con archivos blf.

Además, cuando abre un archivo existente, verifica que el formato sea correcto, incluso si cada bloque tiene una suma de verificación y si esta no es correcta, devolverá un error.

Crearé 2 tipos de archivos BLF:

  1. El Trigger blf

  2. El Spray blf

Ambos son archivos blf pero modificados de manera diferente.

Una imagen de primer plano de código de computadora, descripción generada automáticamente con baja confianzaDe esta manera, el PoC primero crea el archivo "trigger blf", usando CreateLogFile, con la ruta, por ejemplo: LOG:C:\Users\Public\1280 que he configurado antes y que se almacenó en la variable stored_name_CreateLog.

El quinto argumento fCreateDisposition, como en CreateFileA(), puede tomar los siguientes valores:

Una imagen que contiene texto, fuente, línea, recibo, descripción generada automáticamente

En este caso usaré el argumento OPEN_ALWAYS, por lo que el archivo se creará si no existe y si existe se abrirá. Como el archivo aún no existe, se creará con un nombre aleatorio.

logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);

CreateLogFile() creará nuestro archivo "trigger blf" con sus 6 bloques y sus correspondientes sumas de verificación y devolverá el manejador que se almacenará en la variable logFile.

Una imagen que contiene texto, captura de pantalla, fuente, número, descripción generada automáticamente

Cada bloque tendrá desde el desplazamiento mostrado en la columna izquierda, un encabezado cuyo tamaño es de 0x70 bytes.

Así, por ejemplo, el encabezado del CONTROL BLOCK va desde el desplazamiento 0x0 hasta 0x70.

Una captura de pantalla de una computadora, descripción generada automáticamente con baja confianza

Todos los encabezados de todos los bloques tienen la misma estructura llamada _CLFS_LOG_BLOCK_HEADER.

Esta es la estructura del encabezado:

Una captura de pantalla de una computadora, descripción generada automáticamente con confianza media

En el desplazamiento 0xC del encabezado puedo encontrar la suma de verificación, por lo que como el CONTROL BLOCK comienza en el desplazamiento 0, la suma de verificación estará en el desplazamiento 0xC del archivo y así cada bloque tendrá su suma de verificación en el 0xC desde el inicio de su bloque.

Una captura de pantalla de una computadora, descripción generada automáticamente con baja confianza

4- Modificar el archivo "trigger blf":

Para modificar el archivo trigger blf, debo abrirlo como un archivo normal ya sea con CreateFileA o con fopen y luego modificarlo con WriteFile o fwrite respectivamente; realizo esto al principio de la función fun_prepare del PoC.

Recuerde que la ruta normal se almacena en la variable stored_name_fopen, por lo que la uso para abrir el archivo con wfopen_s (que es una variante de fopen que admite cadenas Unicode).

El archivo se modifica en la función craftTriggerBlfFile llamada desde fun_prepare.

Una imagen que contiene texto, línea, fuente, captura de pantalla, descripción generada automáticamente

Luego llamo a fseek para apuntar al desplazamiento que se va a cambiar y luego con fwrite se modifica el archivo.

Una captura de pantalla de una computadora, descripción generada automáticamente con confianza mediaLos cambios que se deben realizar en el archivo "trigger blf" son los siguientes:

Después de realizar estos cambios, se llama a FixCRCFile para calcular la nueva suma de verificación y corregir las sumas de verificación de los primeros 4 bloques. Los siguientes dos bloques no tienen cambios, por lo que no es necesario recalcular sus sumas de verificación.

Una imagen que contiene texto, fuente, captura de pantalla, número, descripción generada automáticamente

5- Obtener la dirección en kernel del BASE BLOCK del trigger blf:

El controlador CLFS.sys lee los seis bloques del archivo, y para almacenar su contenido realiza una asignación en el pool del kernel.

Una imagen que contiene texto, captura de pantalla, fuente, número, descripción generada automáticamente

Hay una estructura muy importante de tamaño 0x90 que en el blogpost anterior de CVE-2022-37969, mediante ingeniería inversa encontré algunos campos y la llamé pool_0x90. Después de mucha más ingeniería inversa, ahora sé que su nombre real es m_rgBlocks y a medida que el controlador va asignando memoria para copiar del archivo el contenido de cada bloque, allí guarda el tamaño de cada bloque, el desplazamiento de inicio y la dirección en kernel donde se almacenó.

Una imagen que contiene texto, captura de pantalla, fuente, descripción generada automáticamente

Tiene seis CLFS_METADATA_BLOCK que corresponden a cada bloque por su número.

Cada estructura CLFS_METADATA_BLOCK tiene 0x18 bytes de longitud. (0x18*6=0x90)

Una imagen que contiene texto, fuente, línea, número, descripción generada automáticamenteEn el desplazamiento 0 hay una unión, pero al menos en este exploit solo se usa el campo pbImage, por lo que simplificándolo sería:

La asignación de esa estructura se puede realizar desde dos lugares diferentes del controlador CLFS.sys, según la creación de un nuevo archivo o si se abre uno existente. En el caso de que se cree un nuevo archivo, el controlador asigna los 0x90 bytes desde CClfsBaseFilePersisted::CreateImage+28A, mientras que en el caso de un archivo existente lo asigna desde CClfsBaseFilePersisted::ReadImage+6E.

Después de eso, obtendré la dirección de inicio del bloque 2 que corresponde al archivo trigger blf, llamado BASE BLOCK que comienza en el desplazamiento 0x800 y su longitud es 0x7a00.

Una imagen que contiene texto, captura de pantalla, fuente, número, descripción generada automáticamente

Dentro de la función fun_prepare, más abajo, esta dirección se encontrará en el kernel usando este fragmento de código.

Una captura de pantalla de código de computadora, descripción generada automáticamente con baja confianza

Primero, la función getBigPoolInfo encuentra todas las asignaciones en el pool que tienen la etiqueta "Clfs" y un tamaño de 0x7a00, luego las almacena en un array.

Después de eso, abre nuevamente el archivo trigger blf previamente modificado usando CreateLogFile con el argumento OPEN_EXISTING, por lo que abre un archivo existente; esto realizará la asignación de su BASE BLOCK.

Cuando se llama a getBigPoolInfo nuevamente, habrá un nuevo pool "Clfs" de tamaño 0x7a00, y su dirección se recupera llamando a NtQuerySystemInformation dos veces.

La dirección del BASE BLOCK del archivo trigger blf se almacena en la variable CLFS_kernelAddrArray.

Tenga en cuenta que si el archivo trigger blf modificado no tiene la suma de verificación correcta, la función CreateLogFile() fallará.

6- Llamar a AddLogContainer con el manejador del trigger blf:

La última parte de la función fun_prepare llama a la API AddLogContainer usando el manejador del archivo trigger blf.

Una imagen de primer plano de código de computadora, descripción generada automáticamente con baja confianza

7- Preparar los archivos spray blf:

En la última función del PoC llamada to_trigger se creará un segundo tipo de archivo blf,

Lo llamaré spray blf.

Este tipo de archivo se utilizará para llenar un espacio de memoria (spray), se necesitan 10 iguales de este tipo, pero inicialmente solo se crea uno. Una imagen que contiene texto, fuente, línea, captura de pantalla, descripción generada automáticamente

Se crearán tres arrays para almacenar los nombres aleatorios de estos archivos:

stored_log_arrays: almacena diez nuevos nombres aleatorios de archivos .blf que se usarán con CreateLogFile.

stored_container_arrays: almacena nombres aleatorios para crear diez nuevos archivos contenedores.

stored_fopen_arrays: almacena los nombres de los archivos de registro del primer array (variable stored_log_arrays), pero con su ruta normal (sin la cadena "LOG:") y con la extensión .blf.

Una captura de pantalla de código de computadora, descripción generada automáticamente con confianza media

En cada iteración, el archivo blf se copia usando CopyFileW, se asignan los nombres almacenados en los arrays.

La función fun_trigger llama a craftSprayBlfFile donde se realizan modificaciones a cada archivo y FixCRCFile corregirá los CRCs.

Una captura de pantalla de un programa de computadora, descripción generada automáticamente con confianza media

Resumiendo, he creado 10 archivos similares (spray blf) con nombres aleatorios con las siguientes modificaciones:

Una captura de pantalla de una computadora, descripción generada automáticamente con confianza media

El último cambio es copiar todo el bloque 0 (CONTROL BLOCK) al bloque 1 (CONTROL BLOCK SHADOW)

Una captura de pantalla de una computadora, descripción generada automáticamente con baja confianza

El efecto de estos cambios, más los realizados al archivo trigger blf, se explicarán más adelante en el capítulo de depuración.

Algunos de estos cambios son los que producen la vulnerabilidad, mientras que otros solo son necesarios para eludir las comprobaciones del controlador.

En este punto, los archivos ya están creados y modificados, listos para realizar el spray; luego, cuando se abran con CreateLogFile, se ubicarán en el área de memoria que deseamos, como se mostrará más adelante.

8- Preparar la memoria para realizar el spray

En la función to_trigger, se crea un array de 12 elementos que contiene la dirección del BASE BLOCK del archivo trigger blf más 0x30.

Luego, en la función fun_pipeSpray, se llena la memoria con un spray de pipes; dentro hay un bucle que llama a CreatePipe y crea la cantidad de pipes que se pasa como primer argumento; el segundo argumento es un array que almacenará los manejadores de todos los pipes creados.

Dentro de un bucle, llama a CreatePipe creando pipes de lectura y escritura.

De esta manera, primero se crearán 0x5000 pipes y luego se llamará nuevamente para crear otros 0x4000 pipes.

Luego usa WriteFile para escribir en los primeros 5000 pipes el array recién creado con las direcciones de BASE BLOCK + 0x30 del archivo trigger blf.

Una captura de pantalla de código de computadora, descripción generada automáticamente con baja confianza

Ahora ya tiene un bloque compacto creado en memoria; liberará 0x667 pipes desde el número 0x2000 hasta el 0x2667, ya que en memoria los pipes no están en el mismo orden en que fueron creados, lo que sucederá es que habrá espacios libres en este bloque de memoria.

Una imagen que contiene texto, captura de pantalla, fuente, software, descripción generada automáticamenteNótese que las asignaciones de los pipes tienen un tamaño de usuario de 0x90 bytes, por lo que cuando se liberen, tendremos

Libera los espacios de memoria de tamaño 0x90 entre la memoria llena de pipes.
Luego itera para llamar a CreateLogFile con los 10 archivos spray blf.
Cuando se llama a CreateLogFile para abrir archivos existentes, se realiza la asignación de 0x90 bytes para el m_rgBlocks, uno por cada archivo spray blf, por lo que estas asignaciones ocuparán los huecos que quedaron al liberar los pipes, ya que son del mismo tamaño.

Una captura de pantalla de código de computadora, descripción generada automáticamente con confianza media

Luego repite el proceso de escribir en los 0x4000 pipes finales el array que tiene la dirección de BASE BLOCK + 0x30 de trigger blf.

9- Desencadenar el error

Todas estas manipulaciones crean un espacio de memoria controlado; mostraré cómo es cuando se está depurando, pero la idea es que los m_rgBlocks de cada archivo spray blf ocupen los huecos de 0x90 bytes que se liberaron.

Luego, ya en la parte final, el error se desencadena dentro de un while( 1 ) usando una llamada a AddLogContainer a los archivos spray blf.Una imagen que contiene texto, fuente, línea, número Descripción generada automáticamente

Dentro de este bucle se desencadena el bug:

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con baja confianza

Este bucle saldrá cuando encuentre el token de Sistema, utilizando la función NtFsControlFile que leerá los atributos de los pipes.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Luego, usando CreateLogFile, se sobrescribe de nuevo el token de nuestro proceso con el token de Sistema recién encontrado y de esta forma logramos la elevación de privilegios.

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

Después se restauran algunos valores, se cierran los manejadores de los pipes y los archivos blf, y se ejecuta un Bloc de notas como Sistema para verificar que hemos escalado correctamente.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Nótese los archivos blf creados en la carpeta PÚBLICA. Recuerde que si quiere intentarlo de nuevo, primero debe eliminar los archivos creados. Algunos quedarán bloqueados y no se podrán eliminar, pero el PoC seguirá funcionando.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Depuración:

1- Verificación del memory spray

Antes de empezar con el efecto de los cambios en los archivos trigger blf y spray blf para realizar la explotación, debo verificar que m_rgBlocks de los archivos spray blf estén ubicados en los agujeros que ocurren en la distribución de memoria, después de realizar el spray de pipes y la posterior liberación de un número fijo de pipes.

Cuando este procedimiento termina, un pipe debería estar ubicado debajo de los 0x90 bytes de m_rgBlocks, por lo que cuando se use m_rgBlocks, se producirá un OUT OF BOUNDS y leerá desde ese pipe que está debajo.

El PoC tiene un punto ideal para colocar un breakpoint:

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

En este punto, la apertura de los archivos spray blf está completa y aún no se ha llamado a la función AddLogContainer.

Para depurar en modo usuario, usaré x64dbg y para modo kernel, IDA con el plugin Windbg.

Una captura de pantalla de un ordenador Descripción generada automáticamente con baja confianza

En este punto la memoria ya debería estar preparada, y puedo ver la distribución.

Pausaré IDA para encontrar un punto interesante donde poner un breakpoint.

Configuraré un breakpoint en CClfsBaseFilePersisted::AddContainer, que se llama desde AddLogContainer y al principio tiene el registro RCX apuntando a la estructura CClfsBaseFilePersisted y en el offset 0x30 hay un puntero a m_rgBlocks.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Cuando se alcanza el breakpoint, verifico en la pila de llamadas que AddLogContainer está siendo llamado desde mi PoC.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

El registro RCX apunta a:

Una captura de pantalla de un ordenador Descripción generada automáticamente con baja confianza Una captura de pantalla de un ordenador Descripción generada automáticamente con baja confianza

El primer campo es el puntero a una vtable (CLFS! CClfsBaseFilePersisted::'vftable') y en el offset 0x30 está el puntero a m_rgBlocks.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con confianza media

Los bloques 0, 1, 4 y 5 aún no han guardado el pbImage, mientras que los bloques 2 (BASE BLOCK) y 3 (SHADOW BLOCK) sí lo tienen.

Cada bloque en la tabla m_rgBlocks tiene su cbOffset que es el offset donde comienza el bloque en el archivo, cbImage es el tamaño del bloque, y eBlockType es el tipo de bloque.

Si el spray es correcto, debajo de m_rgBlocks debería haber un pipe y dentro, los punteros a BASE BLOCK + 0x30 del trigger blf.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

El comando "!pool" en windbg muestra la distribución de memoria:

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Cada m_rgBlocks tiene una etiqueta “Clfs” y su tamaño es 0xa0 porque es el tamaño de usuario 0x90 más 0x10 de cabecera, y debajo hay un pipe con la etiqueta “NpFr” que tiene el mismo tamaño de usuario 0x90 + 0x10 de cabecera.

Dado que la distribución no es una ciencia exacta, algunos “Clfs” se colocaron de forma continua, lo cual no es deseable, pero el que estoy manejando está colocado correctamente seguido de un pipe.

2-Mirando el RecordOffset[12] de trigger blf

Uno de los primeros cambios que afecta es el realizado en el archivo trigger blf en el offset 0x858, donde se almacena el valor 0x369.

Un primer plano de un letrero Descripción generada automáticamente con baja confianza

El BASE BLOCK comienza en el offset 0x800 del archivo.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Dentro del _CLFS_LOG_BLOCK_HEADER en el offset 0x800+0x58 (0x58 desde el inicio de la cabecera del BASE BLOCK).

Una imagen que contiene texto, captura de pantalla, fuente, visualización Descripción generada automáticamente

En el offset 0x28 comienza el array RecordOffsets (DWORD).

Avanzando 0x30 bytes, en el offset 0x58 (0x828+0x30=0x858 desde el inicio), está el campo 12 de RecordOffsets.

Una imagen que contiene texto, fuente, captura de pantalla, gráficos Descripción generada automáticamente

Ejecuto el PoC hasta CreateLogFile como se muestra en la imagen siguiente:

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Una captura de pantalla de un ordenador Descripción generada automáticamente

Antes de entrar a CreateLogFile voy a poner un breakpoint en un lugar donde el valor 0x369 aún no se haya utilizado.

En el caso de que CreateLogFile abra un archivo existente, la estructura m_rgBlocks se asigna aquí:

CClfsBaseFilePersisted::ReadImage+6E

Por lo tanto, pondré un breakpoint en IDA justo aquí:

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Cuando se dispara el breakpoint**:**

Una imagen que contiene texto, captura de pantalla, fuente, número Descripción generada automáticamente

En m_rgBlocks todavía hay basura porque aún no está inicializado, pero tan pronto como se asigna pbImage del bloque 2, la dirección se guardará en el offset 0x30 desde el inicio, ya que el primer campo dentro de cada CLFS_METADATA_BLOCK es pbImage.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Ahora configuro un breakpoint por hardware en escritura: ba w1 ffffd003'7f5bea30

Después de inicializar a cero, se detiene cuando guarda pbImage.

El análisis indica que corresponde al block0, porque no considera la constante r14*8 que es 0x30 después, por lo que en realidad está escribiendo el pbImage del block 2.

Una imagen que contiene texto, captura de pantalla, fuente Descripción generada automáticamente

Nótese que CClfsBaseFilePersisted::ReadMetadataBlock se usa para asignar cualquiera de los bloques, usando el tamaño pasado como argumento.

Una imagen que contiene texto, fuente, captura de pantalla, línea Descripción generada automáticamente

Ahora configure un breakpoint de lectura/escritura en 0x58 desde el bloque base, para ver cuándo se usa el valor 0x369.

ba r1 FFFF978A'16ECF000+0x58

Cuando se alcanza el breakpoint, lee el valor 0x369 ubicado en RecordOffset[12], lo suma a un puntero extraño en r14 e incrementa el contenido de RAX+r14.

Unas líneas más arriba en el código, ESI tiene el valor 0x13 y lo multiplica por 0x18, que es el tamaño de cada bloque en m_rgBlocks.

WINDBG>? 0x18*0x13

Evaluate expression: 456 = 00000000'000001c8

Si sumo el valor de r8= 0x1c8 que es mayor que 0x90 a la dirección inicial de m_rgBlocks, estará leyendo OUT OF BOUNDS.

Una captura de pantalla de un ordenador Descripción generada automáticamente

Debajo de m_rgBlocks, está el pipe con el puntero a BASE BLOCK + 0x30, lee este puntero que fue colocado estratégicamente dentro del pipe.

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con baja confianza La posición actual en el código fue llamada desde la sentencia while(1) del módulo principal.

Dentro del archivo spray blf he colocado estratégicamente el valor 0x13 en el offset 0x48a (iFlushBlock).

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

3-Mirando el valor iFlushBlock en el archivo spray blf

En el offset 0x8a del archivo spray blf se ubica iFlushBlock del BLOCK 0, cuyo valor es 4, mientras que el offset 0x48a pertenece a iFlushBlock del BLOCK 1, y su valor es 0x13

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Ahora tengo que averiguar por qué lee iFlushBlock = 0x13 del BLOCK 1 en lugar de iFlushBlock = 4 del BLOCK 0.

4-¿Por qué lee del BLOCK 1 SHADOW en lugar del BLOCK 0 CONTROL?

Si miro hacia atrás para averiguar de dónde proviene el 0x13, veo en la pila de llamadas que WriteMetadataBlock se llama desde CClfsBaseFilePersisted::ExtendMetadataBlock+416, allí el segundo argumento iFlushBlock es EDX=0x13, que viene de r9w.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con baja confianzaUn par de líneas antes, se llamó a CClfsBaseFile::GetControlRecord para recuperar la dirección del BLOCK 0, quizás el problema está aquí, así que reiniciaré y pondré un breakpoint allí.

GetControlRecord llama a CClfsBaseFile::AcquireMetadataBlock quien debería llenar la tabla m_rgBlocks con la dirección del block 0, cuando paso por esta función obtiene la dirección del block 1, por lo tanto, el problema ocurre dentro de CClfsBaseFile::AcquireMetadataBlock.

Al sumar 0x8A a la dirección obtenida, puedo confirmar que el valor 0x13 que pertenece al BLOCK 1 está presente.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con confianza media

Reiniciaré y pondré un breakpoint allí:

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con baja confianza

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

Una captura de pantalla de un ordenador Descripción generada automáticamente

El segundo argumento pasado a AcquireMetadataBlock es cero, corresponde al block 0, va a copiar desde el archivo y almacenar su dirección en m_rgBlocks Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media.

En la enumeración _CLFS_METADATA_BLOCK_TYPE de los tipos de bloque, tienen nombres diferentes a los que usé, pero son los mismos 6 bloques.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Después de verificar que el tipo de bloque es menor que el máximo m_cBlocks=6, guarda un valor de referencia para evitar leer el mismo bloque dos veces.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Se llama a ReadMetadataBlock, el problema de leer el block 1 en lugar del block 0 estaría dentro de esta función.

Una imagen que contiene texto, fuente, número, línea Descripción generada automáticamente

Si todo está bien, asigna usando cbImage como tamaño y almacena la dirección en el campo block 0-> pbImage en m_rgBlocks.

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

El comando !pool muestra la etiqueta y el tamaño asignado.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamentePor lo tanto, ya tengo la dirección de pbImage del block 0 almacenada en m_rgBlocks, así que necesito ver por qué copia los bytes del block 1 allí en lugar de los bytes del block 0.

Llego a una llamada a CClfsContainer::ReadSector donde se pasa un puntero a una variable que contiene pbImage, para escribir los bytes.

Nótese los cambios realizados en el contenido de pbimage al pasar por ReadSector.

Sumando 0x8a a pbImage puedo encontrar el valor 4 que es correcto, en lugar de 0x13, por lo que el problema debe ocurrir más adelante.

Después de llamar a ClfsDecodeBlock devuelve un error 0x0C01A000A.

CClfsBaseFilePersisted::ReadMetadataBlock+153 llama a ClfsDecodeBlock

Después de este error, suma 1 al tipo y llama a CClfsBaseFilePersisted::ReadMetadataBlock nuevamente pero con tipo 1 para leer el block 1.

Una captura de pantalla de un ordenador Descripción generada automáticamente

En CClfsBaseFilePersisted::ReadMetadataBlock asigna y almacena un nuevo pbImage en m_rgBlocks para el block 1.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Los Blocks 0 y 1 tienen direcciones diferentes, ahora si sumo 0x8a a la dirección del block 1 su valor es 0x13.

Quizás como block 0 devolvió un error, usa el block 1 y lo devuelve a GetControlRecord como Control Block.

Como se mostró antes, cuando se usa el valor 0x13 en lugar de 4, se sale de los límites de m_rgBlocks y lee los valores del spray de pipes controlados por mí.

Una imagen que contiene texto, captura de pantalla, fuente Descripción generada automáticamente Luego libera el pbImage del block 0 y copia el puntero del block 1 al block 0.

Una captura de pantalla de un ordenador Descripción generada automáticamente

Sería necesario encontrar el valor que causa el error 0x0C01A000A dentro de ClfsDecodeBlock.

Dentro de ClfsDecodeBlock el checksum del primer bloque es cero, este es el error 0xC01A000A.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

5-¿Por qué el checksum es igual a cero en los archivos blf spray?

Antes de llamar a AddLogContainer, al abrir cualquier archivo spray blf con un editor hexadecimal, el checksum se había cambiado a cero.

Una captura de pantalla de un ordenador Descripción generada automáticamente

debería haberse cambiado antes cuando se abrió con CreateLogFile.

Una imagen que contiene texto, captura de pantalla, visualización, fuente Descripción generada automáticamente

Por alguna razón, los archivos spray blf terminan después de salir de CreateLogFile con el checksum del block 0 igual a 0 y devuelven un handle válido, veamos por qué sucede esto.

Me detengo en CreateLogFile antes de abrir algún archivo spray blf.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Nótese que antes de llamar a CreateLogFile, los archivos spray tienen el checksum correcto en block 0 y después de completar la función, el valor del checksum cambia a cero.

Una captura de pantalla de un código de ordenador Descripción generada automáticamente con baja confianza

Por lo tanto, configuro un breakpoint en CClfsBaseFile::GetControlRecord, para mirar dentro.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente Después de pasar CClfsContainer::ReadSector el checksum no es cero.

Antes de entrar a calcular el CRC32, pone el campo checksum a cero en memoria para calcular el CRC, y el resultado es correcto.

Una imagen que contiene texto, fuente, captura de pantalla Descripción generada automáticamente

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

Luego verifica el valor de eExtendState =2 y va a WriteMetadataBlock.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Aquí el checksum sigue siendo cero en memoria, solo necesito ver cuándo se escribe este valor en el archivo.

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

Verifica algunos valores que están manipulados en el archivo blf spray para llegar a CClfsBaseFilePersisted::ExtendMetadataBlock.

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con confianza media

Después de un bucle para leer bloques que aún no se han leído, el block 0 continúa con checksum = 0.

Un rectángulo blanco con texto negro Descripción generada automáticamente con baja confianza

Llegando a WriteMetadataBlock.

Como estoy ejecutando antes de que reemplace el block 0 con el 1, el valor iFlushBlock del archivo blf spray sigue siendo 4, el valor correcto.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Ahora está trabajando con el block 4, y escribirá el block 4 en el archivo, aquí aún no está el problema.

Luego llega a CClfsBaseFilePersisted::FlushControlRecord

Una captura de pantalla de un programa de ordenador Descripción generada automáticamente con confianza media

Dentro de ella llega a WriteMetadataBlock, pero con argumento 0, para escribir el block 0 en el archivo**.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Luego ClfsEncodeBlock devuelve el error 0xC01A000A, aunque escribirá el archivo con el block 0 incorrecto en CClfsContainer::WriteSector, justo debajo.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

La variable var_54 almacena el valor de error 0xC01A000A y se verificará antes de salir de la función.

Una captura de pantalla de un ordenador Descripción generada automáticamente con confianza media

Pero después de llamar a CClfsContainer::WriteSector que no devuelve error, el contenido de var_54 se sobrescribe con cero.

Por lo tanto, la función devuelve cero sin error y continúa funcionando ya que CreateLogFile devolverá un handle en lugar de un valor de error.

Una captura de pantalla de un ordenador Descripción generada automáticamente

6-Finalizando la explotación.

El valor 0x13 en iFlushBlock hace que se salga de los límites y leerá el puntero que está en los pipes que apunta al Base Block +30 del trigger blf.

Una captura de pantalla de un ordenador Descripción generada automáticamenteLuego añade 0x28 a ese puntero, ( 0x58 desde el inicio del bloque base del trigger blf) que tiene el valor 0x369.

Una captura de pantalla de un programa de computadora Descripción generada automáticamente con confianza media

La instrucción INC incrementará el valor 0x14 en 1 y se repite 4 veces, por lo que 0x14 termina en 0x18.

WINDBG>db r14+369

ffffcb82'091e7397 14 00 00 00

Después de eso, se llama a CreateLogFile y lee el valor 0x1858.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

Una imagen de cerca de una tarjeta Descripción generada automáticamente con baja confianzaGetSymbol verifica si el bloque falso creado previamente en trigger blf, apuntado por el offset 0x1858, tiene los valores correctos.

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

si el puntero no hubiera sido incrementado varias veces, tendría el valor original 0x1458 y apuntaría al bloque correcto.

Después de salir de GetSymbol, usará ese bloque falso aquí.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

Luego leerá el valor del offset 0x18 del bloque falso donde coloco 0x05000000 y saltará al contenido de lo que hay allí.

WINDBG>dps 0x5000000

00000000'05000000 00000000'05001000

Lee el contenido de 0x05000000 y su 0x05001000 y allí está ClfsEarlierLsn.

Una captura de pantalla de un programa de computadora Descripción generada automáticamente con baja confianza

Esta función se utiliza para devolver el valor 0xFFFFFFFF en RDX aunque esta primera vez ese valor no se utiliza.

La segunda llamada ocurre aquí, llama a PoFxProcessorNotification que estaba en 0x501000 +8

Una imagen que contiene texto, fuente, captura de pantalla, línea Descripción generada automáticamente Una imagen que contiene texto, fuente, captura de pantalla, línea Descripción generada automáticamente

WINDBG>dps 00000000**'05001000**

00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn

00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

Una captura de pantalla de una pantalla de computadora Descripción generada automáticamente con baja confianza

en esta función RCX = 0x05000000 , verifica que 0x40 bytes después deben ser distintos de cero

WINDBG>dps rcx+40

00000000'05000040 00000000'05000000

La dirección a la que saltar será 0x68 después.

WINDBG>dps rcx+68

00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient

Y el argumento será 0x48 bytes después.

WINDBG>dps rcx+48

00000000'05000048 00000000'05000400

El ClfsMgmtDeregisterManagedClient, es una función conveniente porque puedo controlar el argumento y también tengo dos saltos a funciones controladas por mí.

Una captura de pantalla de una computadora Descripción generada automáticamente

La primera llamada es nuevamente a ClfsEarlierLsn que devolvió RDX=0xFFFFFFFF.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

tomará la fuente a escribir del contenido de RDX=0xFFFFFFFF.

WINDBG>dps rdx

00000000'ffffffff ffff8005'3a4ee000

En la dirección 0xFFFFFFFF tenía almacenado el system_EPROCESS & 0xfffffffffffff000.

Una imagen que contiene texto, fuente, línea, número Descripción generada automáticamente

El destino es el puntero ubicado en 0x5000400 +0x48

*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Una captura de pantalla de una computadora Descripción generada automáticamente

El puntero PipeAttribute en kernel que apunta a un búfer lleno de "A" será sobrescrito con la parte alta del puntero SYSTEM EPROCESS.

Este puntero se creó cuando previamente llamé a _NtFsControlFile con un búfer lleno de "A" .

Una captura de pantalla de un programa de computadora Descripción generada automáticamente con confianza media

El contenido de ese atributo se puede leer usando NtFsControlFile.

Una captura de pantalla de una pantalla de computadora Descripción generada automáticamente con confianza media

Ahora el atributo de pipe ya no apunta al búfer con "A" sino a system_EPROCESS & 0xffffffffffffff000.

Una captura de pantalla de un programa de computadora Descripción generada automáticamente con baja confianza

Este código se repetirá hasta que se recupere el token del sistema.

Una captura de pantalla de un código de computadora Descripción generada automáticamente con confianza media

Una captura de pantalla de una computadora Descripción generada automáticamente

En Windows 11, el token del sistema está en el offset 0x4b8 de la estructura EPROCESS recién leída.

Una captura de pantalla de una computadora Descripción generada automáticamente

Solo necesito escribir ese token del sistema en mi proceso llamando a CreateLogFile.

Una imagen que contiene texto, fuente, línea, captura de pantalla Descripción generada automáticamente

Para hacer este trabajo, solo repito el paso utilizado para leer el token del sistema.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

En la doble llamada, primero llama a ClfsEarlierLsn para devolver 0xFFFFFFFF en RDX y luego llama a nt_SeSetAccessStateGenericMapping.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Verifico que el valor apuntado por RDX sea el Token del Sistema.

Una imagen que contiene texto, captura de pantalla, fuente Descripción generada automáticamente

El token de mi proceso es:

Va a escribir allí.

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'f601c06c

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'ef841919

Ahora mi proceso es System. Puedo ejecutar un Notepad para verificar.

Una imagen que contiene texto, captura de pantalla, fuente, línea Descripción generada automáticamente

Una captura de pantalla de una computadora Descripción generada automáticamente

Una captura de pantalla de una computadora Descripción generada automáticamente

7-El parche real

BINDIFF muestra muchas funciones cambiadas

Una captura de pantalla de una computadora Descripción generada automáticamente

La función vulnerable está aquí:

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

Una captura de pantalla de una computadora Descripción generada automáticamente

La primaria es la versión parcheada, la secundaria es la versión vulnerable.

El parche prueba el valor de retorno de CflsEncodeBlock, que es 0xC01A000A, lo almacena en la variable var_54, y como es negativo, lo verifica y evita el WriteSector.

El parche, además de no escribir el archivo, la función retorna correctamente 0xc01a000a, con lo cual CreateLogFile no devuelve ningún handle y la explotación no puede continuar.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza media

Una captura de pantalla de una computadora Descripción generada automáticamente

Solo si ClfsDecodeBlock no es negativo, va a WriteSector pero deja retornando el valor negativo 0xC01A000A.

Una captura de pantalla de una computadora Descripción generada automáticamente con confianza mediaEste es el parche real que realmente previene la explotación usando el PoC que acabo de adjuntar.

En este punto hemos explicado cómo se explotó el bug, lleva a controlar las funciones que nos permiten leer el token SYSTEM y escribirlo en nuestro propio proceso para lograr la escalada de privilegios local. Puedes encontrar el PoC funcional en Fortra’s GitHub.

Esperamos que te sea útil, si tienes alguna duda puedes contactarnos:

[email protected] 
@ricnar456

 [email protected]
@solidclt

Descargar herramienta