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.
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.
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 se derivan ambos de la clase baseFSRegObject no implementa una función vtable de bloqueo, y la implementación de la clase base () es simplemente una instrucción nop. Esto significa que en realidad no se implementa ningún mecanismo de bloqueo para los objetos . Por el contrario,FSStreamReg:
Bloque de código 1: FSRendezVousServer::Close, que bloquea y desbloquea una ruta para FsRegObjects
Hay dos objetos derivados de la clase base
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 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 argumentolpInBufferFSRegObject se almacena en
Irp->CurrentStackLocation->FileObject->FsContext2.
El mismo puntero al objeto FSRegObject se almacena dos veces en FsContext2, una vez en el utilizado por el proceso de inicialización y otra vez en el utilizado por el proceso de registro. De esta manera, las referencias al objeto 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 se inicializa a 1 y luego se incrementa de nuevo una vez completada la inicialización. El registro de aumenta su recuento de referencias nuevamente, para un total de tres referencias por objeto.
Diagrama 2: Inicialización y registro de objetos FSContextReg
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
En MSKSSRV, se llama a
Bloque de código 3: FSRendezvousServer::Close, procesar comprobaciones en FSRegObjects
La información específica del proceso se almacena en el
Irp->CurrentStackLocation->FileObject->FsContext2
en el momento de la inicialización o el registro. El controlador determina qué procesa recursos específicos (
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
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 mismo
Diagrama 3: uso compartido del identificador del dispositivo MS KS Server
Debido a esto, también es posible que el contexto del proceso durante
Diagrama 4: Proceso desconocido que cierra el último identificador de un FILE_OBJECT
Noté que la función
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 , su recuento de referencias se ha reducido a cero. La última vez que se accede a un válido durante las solicitudes IRP es en una llamada a . Por lo tanto, si es posible un uso después de la liberación, siempre se producirá una llamada a 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 , 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 apunta a la misma memoria en ambos representados por los identificadores correspondientes.
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
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
Bloque de código 5: FSRegObject::CloseInitProcess establece el puntero FSContext en NULL
Esto significa que
Sin embargo, si el proceso de llamada es un proceso desconocido, no se produce limpieza y se llama a
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
Esto da un total de cuatro referencias anuladas en un único objeto
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 , se comprueba que el puntero en 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 . Durante la cuarta convocatoria a , 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 . El fragmento de código siguiente muestra la primitiva de uso después de la liberación que se puede obtener mediante la vulnerabilidad:
Bloque de código 6: ruta primitiva de UAF
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.
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
En la sección de vulnerabilidades, mencioné que la causa final de este error fue no configurar
Irp->CurrentStackLocation->FileObject->FsContext2
a
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:
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,
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, 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 , 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.
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
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,
Echemos un vistazo a
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 . 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 , 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.
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.
Manténgase al día sobre las tendencias más importantes e intrigantes de la industria sobre IA, automatización, datos y más con el boletín Think. Consulte la Declaración de privacidad de IBM.
Andréa Piazza, por los asombrosos diagramas
Emma Kirkpatrick, por explicarme pacientemente el modelo de seguridad de Windows