Corriendo una y otra vez: el pequeño error que pudo

Una mujer joven se ve contemplativa mientras trabaja en una computadora por la noche.

El pequeño error que pudo: CVE-2024-30089 es una vulnerabilidad sutil del kernel que utilicé para explotar una máquina Windows 11 completamente actualizada (con todas las mitigaciones de seguridad basadas en virtualización y seguridad de hardware habilitadas) y obtuve mi primera victoria en Pwn2Own este año.

En este artículo, describo mi enfoque directo para la caza de errores: elegir un punto de partida y seguir intuitivamente un camino hasta que algo llame mi atención. Este error es interesante porque puede activarse de manera confiable debido a un error lógico. El error se produce en un estado específico dentro de un sistema de comunicación entre procesos, lo que provoca un uso después de la liberación. Para encontrar el error, fue necesario comparar las rutas del código del programa en sus distintos estados posibles, un proceso que describo con detalle. Igualmente intrigante es el origen del error y el enfoque de Microsoft para solucionarlo. Estos temas también se tratan en esta publicación.

A la caza de los días 0: ¿por dónde empezar?

Una pregunta común que recibo sobre la investigación de vulnerabilidades es cómo comenzar. De hecho, elegir un objetivo y apegarse a él podría ser uno de los pasos más difíciles del proceso de investigación. La vulnerabilidad que se comenta aquí está en el Microsoft Kernel Streaming Service (mskssrv.sys). Consulte esta entrada en el blog para obtener una visión general del subsistema. En esa publicación, señalé algunas características del subsistema MSKSSRV que podrían convertirlo en una buena superficie de ataque, específicamente su mecanismo de comunicaciones entre procesos (IPC).

La base de código de MSKSSRV es bastante pequeña, y la última vulnerabilidad en este subsistema que descubrí también fue explotada de forma independiente y normal como un día 0. También escuché sobre esfuerzos adicionales de otros investigadores y empresas para auditar este controlador. Debido a esto, inicialmente caí en la trampa común de asumir que no quedan más errores para encontrar en esta superficie de ataque. Pero, porque lo había sugerido en mi anterior entrada en el blog, elegí confiar en mi instinto y continuar analizando.

Bloqueo bloqueo: ¿Quién está ahí?

Una excelente manera de obtener nuevas ideas de investigación es mantenerse informado sobre la investigación actual. Leí una excelente entradaen el blog de k0shl que me inspiró a buscar un tipo concreto de error. En la vulnerabilidad encontrada por k0shl, el recuento de referencias de un objeto se inicializa e incrementa sin un bloqueo adecuado, creando un uso después de la ventana libre. A pesar de que el error de k0shl es un error de usuario y no está en el kernel, el estilo de programación de la biblioteca vulnerable me recordó a cuando audité anteriormente el controlador MSKSSRV.

El servidor MS KS (MSKSSRV) interactúa con un proceso de usuario a través de objetosFSStreamReg y FSContextReg . FSStreamReg y FSContextReg se derivan ambos de la clase baseFSRegObject . He observado que FSContextReg no implementa una función vtable de bloqueo, y la implementación de la clase base (FSRegObject ) es simplemente una instrucción nop. Esto significa que en realidad no se implementa ningún mecanismo de bloqueo para los objetos FSContextReg . Por el contrario,FSStreamReg implementa una función de bloqueo que utiliza un mutex. El mecanismo de bloqueo se utiliza al acceder a los objetos para la limpieza, en la función FSRendezvousServer::Close :

Captura de pantalla de una función FSRendezvousServer::Close

Bloque de código 1: FSRendezVousServer::Close, que bloquea y desbloquea una ruta para FsRegObjects

Hay dos objetos derivados de la clase base FSRegObject  , pero un tipo de objeto implementa un mecanismo de bloqueo adecuado y el otro no. Esto me pareció sospechoso. Para probar mi teoría, intenté activar un comportamiento indefinido usando referencias al objeto FSContextReg sin protección. Sin embargo, debido a las protecciones de bloqueo en el objeto global FSRendezvousServer , que contiene los punteros a las listas de FSRegObjects , no pude activar nada interesante, a pesar de la falta de protección de bloqueo en los objetos FSContextReg . Aun así, podía sentir que había algo “sospechoso” en el sistema de recuento de referencias para FSRegObjects . Simplemente no sabía qué todavía.

IPC en el servidor MS KS

Como mencioné en mi última entrada en el blog, el aspecto de intercambio de objetos entre procesos de MSKSSRV es una vía interesante para las vulnerabilidades, por lo que decidí centrarme más en ello. El mecanismo IPC del subsistema se ilustra en el siguiente diagrama:

Diagrama del mecanismo IPC del subsistema

Diagrama 1: Comunicación entre procesos en MS KS Server

Abrir un identificador de archivo en el dispositivo MSKSSRV, mediante CreateFile, crea un FILE_OBJECT que corresponde a ese identificador. Con ese identificador, un proceso puede inicializar un nuevo flujo o un objeto de contexto enviando al dispositivo un IOCTL usando la función DeviceIoControl. El proceso de inicialización designa qué proceso remoto puede registrar el objeto especificando el ID del proceso a través del argumentolpInBuffer . El proceso remoto, utilizando un nuevo identificador de archivo para el dispositivo MSKSSRV, ahora puede registrar el objeto a través del IOCTL del dispositivo. El puntero para FSRegObject se almacena en

Irp->CurrentStackLocation->FileObject->FsContext2.

El mismo puntero al objeto FSRegObject se almacena dos veces en FsContext2, una vez en el FILE_OBJECT utilizado por el proceso de inicialización y otra vez en el FILE_OBJECT utilizado por el proceso de registro. De esta manera, las referencias al objeto FSRegObject se pueden compartir entre procesos. Por ejemplo, con un objeto FSStreamReg, varios procesos pueden tener acceso al búfer de tramas de flujo, como se muestra en el diagrama 1. El recuento de referencia de un FSRegObject se inicializa a 1 y luego se incrementa de nuevo una vez completada la inicialización. El registro de FSRegObject aumenta su recuento de referencias nuevamente, para un total de tres referencias por objeto.

Diagrama de inicialización y registro de objetos FSContextReg

Diagrama 2: Inicialización y registro de objetos FSContextReg

Encontrar un error

Decidí volver a revisar lo que había auditado anteriormente para intentar encontrar vulnerabilidades de bloqueo. Observé que la funciónFSRendezvousClose, , que llama aFSRendezvousServer::Close , se invoca dentro de las rutinas de limpieza y cierre de envío del controlador:

Captura de pantalla de un CodeBlock con rutinas DispatchCleanup y DipatchCleanup para el controlador MS KS Server

Bloque de código 2: rutinas DispatchCleanup y DipatchCleanup para el controlador MS KS Server

Una rutina de envío gestiona uno o más tipos de IRP, que son solicitudes de E/S empaquetadas. En Windows, cuando se han cerrado todas las referencias de identificador a un archivo, el controlador de sistema de archivos correspondiente para el archivo recibe una solicitud IRP_MJ_CLEANUP y IRP_MJ_CLOSE  , que son manejadas por las rutinas de función DispatchCleanupDispatchClose del controlador.

En MSKSSRV, se llama a  FSRendezvousClose dentro de las rutinas de función DispatchCleanupDispatchClose del controlador; si el puntero está almacenado en Irp->CurrentStackLocation->FileObject->FsContext2 no es NULL. Algo que me llamó la atención enFSRendezvousServer::Close , que había analizado antes, fueron las diversas comprobaciones del ID de proceso de quien llama. El contexto del proceso importa porque todo el código en modo kernel opera dentro de un espacio de dirección único del kernel, que está separado de los espacios de dirección en modo usuario. Cada proceso tiene su propio contexto de memoria en modo usuario, y el contexto del proceso en el que se ejecuta un hilo del kernel determina a qué espacio de direcciones de usuario del proceso se accederá si el hilo accede a las direcciones de usuario. El siguiente código verifica si el proceso de llamada es el proceso de inicialización o registro:

Captura de pantalla de una función FSRendezvousServer::Close

Bloque de código 3: FSRendezvousServer::Close, procesar comprobaciones en FSRegObjects

La información específica del proceso se almacena en el FSRegObject almacenado en

 Irp->CurrentStackLocation->FileObject->FsContext2

 
en el momento de la inicialización o el registro. El controlador determina qué procesa recursos específicos (

EPROCESS

 objetos, objetos de evento y otras cosas) que necesita liberar comprobando el ID de proceso de quien llama. Si el proceso es el de inicialización o registro, se realiza una limpieza adicional para esos recursos específicos del proceso.

Esto me llamó la atención porque, en general, todas las rutinas Dispatch se ejecutan en un contexto de proceso arbitrario, con algunas excepciones. En otras palabras, el sistema elige un hilo para hacer el trabajo Dispatch ; qué hilo elige es arbitrario. Descubrí que se llama a DispatchCleanup  en el contexto del proceso del proceso que cerró el identificador final. Entonces, en este caso, las verificaciones de ID de proceso tienen sentido. Sin embargo, se llama a DispatchClose  desde un  contexto de proceso arbitrario. Eso significa que si se llama aFSRendezvousServer::Close como resultado de la solicitudIRP_MJ_CLOSE , sería desde un contexto de proceso arbitrario, anulando el propósito de las verificaciones de ID de proceso. Esta fue una gran pista de que algo andaba mal aquí.

Además, como característica del sistema operativo Windows, los identificadores se pueden compartir con otros procesos (mediante la herencia de procesos secundarios o utilizando la función API DuplicateHandle). A través del identificador de archivo compartido, el otro proceso también puede interactuar con el mismoFILE_OBJECT .

Diagrama de uso compartido del identificador del dispositivo MS KS Server

 Diagrama 3: uso compartido del identificador del dispositivo MS KS Server

Debido a esto, también es posible que el contexto del proceso durante DispatchCleanup  no sea ni el proceso de inicialización ni el proceso de registro. Si alguno de esos identificadores se duplica y se comparte con otro proceso, y ese proceso es el último en cerrar el identificador, se llamará a DispatchCleanup  dentro del contexto de ese proceso desconocido (que no es el proceso de inicialización o registro).

Diagrama del proceso desconocido que cierra el último identificador de un FILE_OBJECT

Diagrama 4: Proceso desconocido que cierra el último identificador de un FILE_OBJECT

Noté que la función FSRendezvousServer::Close podría llamarse dos veces cuando se cierra un identificador de archivo (una para la solicitud IRP_MJ_CLEANUP y otra para la solicitud IRP_MJ_CLOSE , bloque de código 2). Dentro de esa función hay dos posibles llamadas a FSRegObject::Release  (bloque de código 4), que deshacen la referencia al objeto y liberan su memoria si el recuento de referencias cae a 0. Eso significa que podría llamarse a FSRegObject::Release  hasta cuatro veces para un identificador. Además, hay dos identificadores de archivo para el cual FILE_OBJECT  apunta al mismo FSRegObject a través del puntero FSContext2. Eso significa la posibilidad de llamar a FSRegObject::Release  hasta ocho veces en el mismo objeto, cuatro para cada identificador. Si el recuento de referencias original del objeto es solo tres, podría haber un posible uso después de la liberación desencadenado por demasiadas referencias anuladas. Esa era mi línea de pensamiento, de todos modos. Sabía que la estructura del programa probablemente evitaría alcanzar el número máximo teórico de referencias anuladas, pero tal vez no lo suficiente. En ese momento sentí que estaba en lo cierto y decidí investigar más a fondo.

Captura de pantalla de Se puede llamar a FSRegObject::Release dos veces desde FSRendezvousServer::Close

Bloque de código 4: Se puede llamar a FSRegObject::Release dos veces desde FSRendezvousServer::Close

En general, más referencias anuladas que referencias en un objeto no es la única forma en que puede ocurrir un uso después de la liberación. Sin embargo, en este caso, podemos estar seguros de que si se ha liberado un FSRegObject , su recuento de referencias se ha reducido a cero. La última vez que se accede a un FSRegObject válido durante las solicitudes IRP IRP_MJ_CLEANUP/CLOSE es en una llamada a FSRegObject::Release . Por lo tanto, si es posible un uso después de la liberación, siempre se producirá una llamada a FSRegObject::Release después de que el objeto ya se haya liberado. Durante la llamada, se volverá a anular la referencia al objeto. Por esa razón, contar el número de referencias anuladas es una buena heurística para encontrar usos luego de las liberaciones para este caso particular.

Lo único que quedaba era trazar los posibles estados del programa, anotando cuándo se libera y accede al objeto. Hice esto emulando mentalmente la lógica del programa durante las solicitudes IRP_MJ_CLEANUP/CLOSE , cada una de las cuales comienza con las funciones Dispatch correspondientes (bloque de código 2), para cada uno de los casos posibles.

A continuación se muestran los estados en función de los cuales el proceso cierra la referencia final a un identificador. Cada entrada representa el número de referencias anuladas del objeto FSContextReg que se producen si el proceso correspondiente cierra el identificador final. Nota: no hay diferencia funcional entre HANDLE #1 (identificador de inicialización) y HANDLE #2 (identificador de registro), ya que el campo FileObject->FSContext2 apunta a la misma memoria en ambos FILE_OBJECT representados por los identificadores correspondientes.

Captura de pantalla de las referencias anuladas de FSContextReg para cada uno de los posibles estados de MSKSSRV IPC

Referencias anuladas de FSContextReg para cada uno de los posibles estados de MSKSSRV IPC

¡Correcto! El último estado da como resultado cuatro referencias anuladas: dos por el proceso desconocido y dos por el proceso de inicialización (y también de registro), mientras que inicialmente solo tiene tres referencias, lo que significa que es posible un uso después de la liberación. También repetí el mismo ejercicio con objetos FSStreamReg , pero debido a un error de pérdida de memoria en el código, en realidad nunca es posible liberar un objeto FSStreamReg después de haberse registrado.

La vulnerabilidad

Mientras realizaba el ejercicio mental de la máquina virtual descrito anteriormente, descubrí el problema. Si el proceso es el proceso de inicialización o el proceso de registro, se lleva a cabo la limpieza adecuada y el puntero almacenado en Irp->CurrentStackLocation->FileObject->FsContext2 se establece en NULL . El bloque de código siguiente muestra dónde ocurre esto en el caso en que quien llama sea el proceso de inicialización:

Captura de pantalla de FSRegObject::CloseInitProcess establece el puntero FSContext en NULL

Bloque de código 5: FSRegObject::CloseInitProcess establece el puntero FSContext en NULL

Esto significa que FSRendezvousClose no se volverá a llamar durante la finalización de la solicitud IRP_MJ_CLOSE en DispatchClose (SrvDispatchClose , bloque de código 2).

Sin embargo, si el proceso de llamada es un proceso desconocido, no se produce limpieza y se llama a FSRegObject::Release  una vez cerca del final de la función. Dado que FileObject->FsContext2 no es NULL, se vuelve a llamar a FSRendezvousServer::Close durante la solicitud posterior IRP_MJ_CLOSE y se produce otra llamada a FSRegObject::Release.

Ahora, si el segundo identificador está cerrado por un proceso que inicializó y registró el objeto FsConteXtreg, el objeto limpiará todos sus recursos de proceso almacenados, quedando vacío. Esto hace que se llame a FSRegObject::Release  dos veces dentro de FSRendezvousServer::Close (bloque de código 4). La referencia anulada adicional sirve para tener en cuenta la referencia de inicialización adicional una vez que el objeto está vacío.

Esto da un total de cuatro referencias anuladas en un único objeto FSContextReg , lo que indica que se produce un uso después de la liberación.

Diagrama de CVE-2024-30089 representado

Diagrama 5: CVE-2024-30089 representado

El lector que sigue el proceso podría preguntarse por qué un proceso desconocido no puede ser el último en cerrar ambos identificadores, ya que esto aparentemente también conduciría a cuatro referencias anuladas. Antes de que el objeto se destruya y libere, se desvincula de una lista almacenada en el objeto FSRendezvousServer global. Al principio de FSRendezvousServer::Close , se comprueba que el puntero en FSContext2 sea un miembro válido de la lista. En este caso, el objeto se libera en la segunda llamada al final de la función FSRegObject::Release . Durante la cuarta convocatoria a FSRendezvousServer::Close , el objeto ya se ha desvinculado de la lista, lo que lo convierte en un objeto no válido, por lo que no se puede usar. Para provocar un uso después de la liberación, debe ocurrir después de que el objeto se haya recuperado y validado en FSRendezvousServer::Close . El fragmento de código siguiente muestra la primitiva de uso después de la liberación que se puede obtener mediante la vulnerabilidad:

Captura de pantalla de la ruta primitiva del código UAF

Bloque de código 6: ruta primitiva de UAF

Complejidad del ataque

En la guía de actualización de seguridad para esta vulnerabilidad, la puntuación CVSS indica que “la explotación es más probable” y la complejidad del ataque para esta vulnerabilidad es “baja”. Si bien Microsoft no proporciona explicaciones detalladas de su puntuación, he notado algunos patrones mientras parcheaba otras vulnerabilidades. La vulnerabilidad probablemente recibió esta puntuación porque se deriva de un error lógico, lo que la hace confiablemente activable. Siguiendo los pasos descritos en el diagrama 5, un atacante puede activar constantemente el escenario de uso después de la liberación descrito en el bloque de código 6. Sin embargo, esto no significa que explotarlo en la práctica sea sencillo. En la siguiente parte de esta serie se cubrirá un recorrido detallado de los pasos de explotación.

Una retrospectiva

Comprender cómo ocurrió un error es importante para cultivar un enfoque proactivo para proteger las prácticas de desarrollo. Para identificar cómo se introdujo la vulnerabilidad, analicé versiones anteriores del controlador obtenidas de Winbindex y busqué diferencias en la lógica en la función FSRendezvousServer::Close.

En la sección de vulnerabilidades, mencioné que la causa final de este error fue no configurar
 

Irp->CurrentStackLocation->FileObject->FsContext2

 

NULL

 si el proceso de llamada es un proceso desconocido. Para mi sorpresa, vi esta línea exacta de código en una versión anterior de mskssrv.sys:

Captura de pantalla del código de la versión anterior de FSRendezvousServer::Close, FsContext2 está establecido en NULL

Bloque de código 7: versión anterior de FSRendezvousServer::Close, FsContext2 está establecido en NULL

En el bloque de código que se muestra arriba, FileObject->FsContext2 se establece explícitamente en NULL , independientemente del resultado de las comprobaciones de ID de proceso anteriores. Esto evita que se vuelva a llamar a FSRendezvousServer::Close en la solicitud IRP_MJ_CLOSE posterior, por lo que no se puede producir la referencia anulada adicional. Extraño: entonces, ¿por qué se quitó esta línea de código? Echemos un vistazo a una versión posterior de la función, donde se introdujo por primera vez el error:

Captura de pantalla de FsContext establecido en NULL dentro de una verificación de indicador de características

Bloque de código 8: FsContext establecido en NULL dentro de una verificación de indicador de características

En el bloque de código anterior se muestra una comprobación del indicador de características.

Feature_Servicing_TeamsUsingMediaFoundationCrashes

.
Los indicadores de características son un componente de Windows que activa varias funcionalidades y experimentos, aunque no hay mucha información pública al respecto. En esta entrada en el blog anterior, vimos cómo los indicadores de características se han utilizado para los parches de vulnerabilidad. Los indicadores de características a veces se utilizan para probar una funcionalidad antes de que se adopte oficialmente. En este caso, si la función

Feature_Servicing_TeamsUsingMediaFoundationCrashes

está habilitada, FileObject->FsContext2 no se establece en NULL, lo que introduce la vulnerabilidad. Se observó que esta característica estaba habilitada de forma predeterminada en las instalaciones de Windows 10. En Windows 11 y como se muestra en el bloque de código 1, este indicador de característica condicional no está presente y el puntero no está establecido en  NULL , lo que también lo hace vulnerable.

 

Debido al nombre de la característica, investigué la funcionalidad de Microsoft Teams, el software de videoconferencias. Confirmé que la aplicación puede usar la funcionalidad MSKSSRV para compartir transmisiones multimedia entre procesos. Es posible que el uso compartido de identificadores de flujo estuviera provocando el cierre de Teams. Un tema interesante para investigar más sería examinar cómo Teams comparte los identificadores de dispositivos MSKSSRV entre procesos y por qué realizar una limpieza adecuada de punteros podría causar que la aplicación se cierre.

El parche

Esta parte de la serie se centra en la vulnerabilidad en sí, que incluye su parche. Me interesaba especialmente examinar el parche para este error, ya que mi arreglo propuesto parecía provocar cierres en Microsoft Teams. Es importante mencionar que, en este punto, aún tengo que examinar cómo una aplicación real utiliza el controlador MSKSSRV en la práctica. No tener este contexto introduce puntos ciegos en la comprensión de por qué un sistema está diseñado de esa manera. Un parche completo para este error requeriría una reestructuración del código base y podría revelar más detalles sobre cómo se pretende que funcione el sistema IPC. También esperaba obtener insight sobre prácticas de programación seguras de los desarrolladores de Microsoft.

Para mi decepción, el error lógico que causó la vulnerabilidad, que se corrigió en las actualizaciones de seguridad de junio de 2024, no se abordó directamente. En su lugar, se agregó una verificación de token de acceso antes de las rutas de código vulnerables. Consulte el siguiente código para el contexto de inicialización IOCTL, manejado por la función FSRendezvousServer::InitializeContext :

La captura de pantalla de la función IOCTL comienza con una verificación de característica y comprueba si el proceso de llamada es un servidor de marco

Bloque de código 9: la función IOCTL comienza con una verificación de indicador de características y comprueba si el proceso de llamada es un servidor de marcos

La función anterior comienza comprobando si una característica está habilitada. Es probable que esta sea la característica correspondiente al parche. Si la característica está habilitada, KsIsCurrentProcessFrameServer debe devolver TRUE; de lo contrario, NTSTATUS  devuelve el valor STATUS_ACCESS_DENIED .

Echemos un vistazo a KsIsCurrentProcessFrameServer :

Captura de pantalla de KsIsCurrentProcessFrameServer realiza una comprobación de SID en el token de acceso del hilo de llamada

Bloque de código 10: KsIsCurrentProcessFrameServer realiza una comprobación de SID en el token de acceso del hilo de llamada

Esta función compara el token del hilo de llamada con dos identificadores de seguridad (SID) específicos. Los SID corresponden a un token en el grupo NT SERVICE\FrameServer . Si cualquiera de los SID está habilitado en el token de acceso del hilo de llamada, entonces se puede ejecutar el código de función vulnerable.

Después de ver esto, sospeché que probablemente todavía había un error Administrator to Kernel. En última instancia, los problemas de corrupción de la memoria no se abordaron en absoluto. Lo confirmé haciendo una ligera modificación a mi exploit original: un usuario administrador puede iniciar el servicio FrameServer , abrir un identificador para el servicio y crear el proceso de exploit utilizando el identificador. Pude obtener una primitiva R/W completa del kernel en un sistema totalmente parcheado.

Si bien Microsoft no considera que Administrator to Kernel sea un límite de seguridad, los actores de amenazas han utilizado errores similares para obtener una primitiva R/W del kernel y usarla para cegar a EDR y las operaciones de rootkit. Si le interesa saber qué tipo de cosas se pueden hacer con este primitivo, consulta mi charla de BlackHat junto con FuzzySec.

Conclusión y siguientes pasos

Esta publicación se centró en la parte de investigación de vulnerabilidades de mi esfuerzo Pwn2Own, que consistió en encontrar una vulnerabilidad de kernel de día 0 que pueda explotarse para la escalada de privilegios. Esta publicación describe el recorrido: obtener ideas de otras investigaciones, no encontrar un error, elegir un nuevo ángulo, encontrar algo sospechoso y finalmente identificar dónde reside la vulnerabilidad. Ahora que se ha identificado un error y hay una primitiva de uso después de la liberación, el resto debería ser sencillo, ¿verdad? Microsoft parece creer que sí, ya que calificó este error como “explotación más probable” con complejidad de ataque “baja”. ¿Tienen razón? Hablaré de eso, de la estrategia de explotación, y desvelaré el significado del título de la serie en la siguiente parte.

Agradecimientos

Andréa Piazza, por los asombrosos diagramas

Emma Kirkpatrick, por explicarme pacientemente el modelo de seguridad de Windows

Mixture of Experts | 12 de diciembre, episodio 85

Decodificación de la IA: Resumen semanal de noticias

Únase a nuestro panel de ingenieros, investigadores, responsables de producto y otros profesionales de talla mundial que se abren paso entre el revuelo de la IA para ofrecerle las últimas noticias e insights al respecto.