El pequeño bug que podría: CVE-2024-30089 es una vulnerabilidad sutil del núcleo 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, expongo mi enfoque directo para la búsqueda de bugs: elegir un punto de partida y seguir intuitivamente un camino hasta que algo llame mi atención. Este bug es interesante porque puede activarse de forma fiable debido a un error lógico. El bug se produce en un estado específico dentro de un sistema de comunicación entre procesos, lo que provoca un use-after-free. Encontrar el bug requirió comparar las rutas de código del programa en sus diversos estados posibles, un proceso que describo en detalle. Igual de intrigante es el origen del bug y el enfoque de Microsoft para solucionarlo. Estos temas también se tratan en esta publicación.
Una pregunta habitual que recibo sobre la investigación de vulnerabilidades es cómo empezar. De hecho, elegir un objetivo y ceñirse a él puede 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). Consulta esta entrada de blog para obtener una visión general del subsistema. En esa publicación, señalo 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 en la naturaleza como un día cero. También me enteré de que otros investigadores y empresas habían hecho esfuerzos adicionales para auditar este controlador. Por esta razon, inicialmente caí en la trampa común de asumir que no quedan más errores por encontrar en esta superficie de ataque. Pero, porque lo había sugerido en mi anterior entrada de blog, decidí confiar en mis instintos y continuar buscando.
Una excelente manera de obtener nuevas ideas de investigación es mantenerse informado sobre las investigaciones actuales. Leí una excelente entrada deblog de k0shl que me despertó la inspiración para ir a la caza un tipo concreto de bug. En la vulnerabilidad encontrada por k0shl, el recuento de referencias de un objeto se inicializa y aumenta sin el bloqueo adecuado, lo que crea una ventana libre de uso posterior. A pesar de que el error de k0shl es un error de usuario y no está en el núcleo, la codificación de la biblioteca vulnerable me recordó a cuando anteriormente audité el controlador MSKSSRV.
El servidor MS KS (MSKSSRV) interactúa con un proceso de usuario a través de los objetosFSStreamReg. y FSContextReg derivan ambas de la clase baseFSRegObject no implementa una función de bloqueo vtable y la implementación de la clase base () es simplemente una instrucción nop. Esto significa que no se implementa ningún mecanismo de bloqueo para los objetos . Por el contrario,FSStreamReg:
Bloque de código 1: FSRendezvousServer::Cerrar, bloquear y desbloquear la ruta para FSRegObjects
Hay dos objetos derivados de la clave base
Como mencioné en mi última entrada de blog, el aspecto de intercambio de objetos entre procesos de MSKSSRV es una vía interesante para las vulnerabilidades, así que decidí centrarme más en ello. El mecanismo de IPC del subsistema se ilustra en el siguiente diagrama:
Diagrama 1: Comunicación entre procesos en el servidor MS KS
Abrir un identificador de archivo en el dispositivo MSKSSRV, vía CreateFile, crea un FILE_OBJECT que corresponde a ese identificador. Usando 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 una vez en el utilizado por el proceso de registro. De esta manera, las referencias al objeto se pueden compartir entre procesos . Por ejemplo, al utilizar un objeto FSStreamReg, varios procesos pueden tener acceso al búfer de tramas de flujo, como se muestra en el Diagrama 1. Un recuento de referencias de se inicializa en 1 y, a continuación, se incrementa de nuevo una vez completada la inicialización. El registro de aumenta de nuevo su recuento de referencias, hasta un total de tres referencias por objeto.
Diagrama 2: inicialización y registro de objetos FSContextReg
Bloque de código 2: rutinas DispatchCleanup y DispatchCleanup para el controlador del servidor MS KS
Una rutina de Dispatch gestiona uno o más tipos de IRP, que son peticiones de E/S empaquetadas. En Windows, cuando se cierran todas las referencias de identificador a un archivo, el controlador del sistema de archivos correspondiente al 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 del proceso de la persona que 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, por lo general, todas las rutinas de Dispatch se ejecutan en un contexto de proceso arbitrario, con algunas excepciones. En otras palabras, el sistema elige un hilo para hacer el trabajo de Dispatch; qué hilo elige es arbitrario. Descubrí que
Además, como característica del sistema operativo Windows, los identificadores pueden compartirse con otros procesos (por herencia de procesos hijos o usando la función API DuplicateHandle). Mediante el identificador de archivos compartidos, el otro proceso también puede interactuar con el mismo
Diagrama 3: Compartir el identificador del dispositivo del servidor MS KS
Por esto, también es posible que el contexto del proceso durante
Diagrama 4: Proceso externo cerrando el último identificador a un FILE_OBJECT
Me di cuenta de que se podría llamar dos veces a la función
Bloque de código 4: FSRegObject::Release se puede llamar dos veces desde FSRendezvousServer::Close
En general, tener más desreferencias que referencias en un objeto no es la única forma en que puede producirse 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, el objeto volverá a ser desreferenciado. Por ese motivo, contar el número de desreferencias es una buena heurística para encontrar usos después de liberaciones en este caso concreto.
Lo único que queda por hacer es rastrear los posibles estados del programa y tomar nota de cuándo se libera el objeto y se accede a él. Lo hice emulando mentalmente la lógica del programa durante las solicitudes , cada una de las cuales comenzaba con las funciones Dispatch correspondientes (bloque de código 2), para cada uno de los casos posibles.
A continuación se muestran los estados según los cuales el proceso cierra la referencia final a un identificador. Cada entrada representa el número de desreferencias del objeto FSContextReg que se producen si el proceso correspondiente cierra el identificador final. Nota: no hay diferencia funcional entre IDENTIFICADOR N.º 1 (identificador de inicialización) e IDENTIFICADOR N.º 2 (idendificador de registro), ya que el campo apunta a la misma memoria en amosb representados por los identificadores correspondientes.
Desreferencias de FSContextReg para cada uno de los posibles estados de IPC de MSKSSRV
¡Hecho! El último estado da como resultado cuatro desreferencias: dos por el proceso externo 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 libre. También repetí el mismo ejercicio con los objetos
Mientras hacía el ejercicio cerebral de la máquina virtual descrito anteriormente, encontré el problema. Si el proceso es el de inicialización o el de registro, se produce la limpieza apropiada, y el puntero almacenado en
Bloque de código 5: FSRegObject::CloseInitProcess establece el puntero FSContext en NULL
Esto significa que no se volverá a llamar a
Sin embargo, si el proceso de llamada es un proceso externo, no se produce ninguna limpieza y se llama a
Ahora bien, si el segundo identificador se cierra mediante un proceso que inicializó y registró el objeto FSContextReg, el objeto limpiará todos los recursos de proceso almacenados, dejándolo vacío. Esto hace que se llame dos veces a
Esto hace un total de cuatro desreferencias en un único objeto
Diagrama 5: muestra el CVE-2024-30089
El lector que lo siga podría preguntarse por qué un proceso extraño no puede ser el último en cerrar ambos identificadores, ya que esto aparentemente también conduciría a cuatro desreferencias. Antes de que el objeto se destruya y libere, se desvincula de una lista almacenada en el objeto globalFSRendezvousServer 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 a al final de la función. Durante la cuarta llamada al , 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 utilizar. Para provocar un uso después de la liberación, debe producirse después de que el objeto se haya recuperado y validado en . El fragmento de código siguiente muestra la primitiva uso después de la liberación que se puede obtener mediante la vulnerabilidad:
Bloque de código 6: ruta primitiva UAF
En la guíade 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”. Aunque Microsoft no proporciona explicaciones detalladas de su puntuación, he observado algunos patrones mientras parcheaba otras vulnerabilidades. Es probable que la vulnerabilidad haya recibido esta puntuación porque se debe a un error lógico, lo que la hace desencadenable de forma fiable. Siguiendo los pasos descritos en el diagrama 5, un atacante puede desencadenar sistemáticamente el escenario de use-after-free representado en el bloque de código 6. Sin embargo, esto no significa que explotarlo sea sencillo en la práctica. En la próxima parte de esta serie se tratarán en detalle los pasos de la explotación.
Comprender cómo se ha producido un fallo es importante para cultivar un enfoque proactivo de las prácticas de desarrollo seguras. Para determinar cómo se introdujo la vulnerabilidad, analicé las versiones anteriores del controlador obtenidas de Winbindex y busqué cualquier diferencia en la lógica de la función
En la sección de vulnerabilidades, mencioné que la causa principal de este error no fue la configuración
Irp->CurrentStackLocation->FileObject->FsContext2
a
si el proceso de llamada es un proceso externo. 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 comprobación de indicador de característica
En el bloque de código anterior se muestra una comprobación de indicador de característica
Feature_Servicing_TeamsUsingMediaFoundationCrashes
.
Los indicadores de características son unos componentes de Windows que activan varias funcionalidades y experimentos, aunque no hay mucha información pública al respecto. En esta entrada de blog anterior, analizamos cómo se han utilizado los indicadores de características para los parches de vulnerabilidad. A veces se utilizan los indicadores de características para probar una funcionalidad antes de que se adopte oficialmente. En este caso, si el
Feature_Servicing_TeamsUsingMediaFoundationCrashes
la característica está activada, no está configurada como NULL, lo que introduce la vulnerabilidad. Se observó que esta característica estaba activada por defecto 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 videoconferencia. Confirmé que la aplicación puede utilizar la funcionalidad MSKSSRV para compartir flujos multimedia entre procesos. Es posible que el uso compartido de identificadores de transmisión estuviera causando el bloqueo de Teams. Un tema interesante para seguir investigando sería examinar cómo Teams comparte los identificadores de dispositivos MSKSSRV entre procesos, y por qué realizar una limpieza adecuada del puntero podría provocar que la aplicación se bloquee.
Esta parte de la serie se centra en la vulnerabilidad en sí, que incluye su parche. Estaba especialmente interesado en examinar el parche para este error, ya que la corrección que propuse parecía provocar bloqueos en Microsoft Teams. Es importante mencionar que, por ahora, aún no he examinado 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 el funcionamiento del sistema IPC. También esperaba obtener algún conocimiento sobre las prácticas de codificació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 cambio, se añadió una comprobación del token de acceso antes de las rutas de código vulnerables. Consulte el código siguiente para el contexto de inicialización IOCTL, gestionado por la función
Bloque de código 9: la función IOCTL comienza con una comprobación de indicador de característica y comprueba si el proceso de llamada es un servidor de marcos
La función anterior comienza por comprobar 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 realizando una comprobación de SID en el token de acceso del subproceso de llamada
Esta función comprueba el token del hilo de llamada con dos identificadores de seguridad (SID) específicos. Los SID corresponden a un token en el grupo . Si alguno de los SID está habilitado en el token de acceso del subproceso de llamada, entonces el código de la función vulnerable puede ejecutarse.
Tras ver esto, sospeché que era probable que siguiera existiendo un error de administrador en el kernel. En última instancia, los problemas de corrupción de memoria no se abordaron en absoluto. Lo confirmé haciendo una pequeña modificación a mi original exploit: un usuario administrador puede iniciar el servicio , abrir un identificador del servicio y crear el proceso de exploit usando el identificador. Pude obtener una primitiva R/W completa del kernel en un sistema totalmente parcheado.
Si bien Microsoft no considera que la conexión entre el administrador y el núcleo sea un límite de boundary, los actores de amenazas han utilizado erroressimilares para obtener una primitiva de R/W del núcleo y utilizarla para cegar EDR (detección y respuesta de endpoints) y realizar operaciones de rootkit. Si está interesado en qué tipo de cosas se pueden hacer con este primitivo, consulta mi charlade BlackHat junto con FuzzySec.
Este post se centró en la parte de investigación de vulnerabilidades de mi proyecto en Pwn2Own, que consistía en encontrar una vulnerabilidad de día cero del núcleo que pudiera aprovecharse para escalar privilegios. Esta publicación describe el camino: inspirarse en otras investigaciones, no ser capaz de encontrar un bug, elegir un nuevo enfoque, encontrar algo sospechoso y, finalmente, identificar dónde reside esa vulnerabilidad. Ahora que se ha identificado un error y hay una primitiva de use-after-free, el resto debería ser sencillo, ¿verdad? Microsoft parece pensar que sí, así que calificaron este error como "explotación más probable" con una complejidad de ataque "baja". ¿Tienen razón? En la siguienteparte hablaré de eso (de la estrategia de explotación) y desvelaré el significado del título de la serie
Manténgase al día sobre las tendencias más importantes e intrigantes del sector en materia de IA, automatización, datos y mucho más con el boletín Think. Consulte la Declaración de privacidad de IBM.
Andréa Piazza, por los increíbles diagramas
Emma Kirkpatrick, por explicarme pacientemente el modelo de seguridad de Windows