Dando vueltas y más vueltas: El pequeño bug que sí pudo

Mujer joven mira contemplativa mientras trabaja en un ordenador por la noche.

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.

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

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.

Toc toc: ¿Quién está ahí?

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 . FSStreamReg y FSContextReg derivan ambas de la clase baseFSRegObject . Me di cuenta de que FSContextReg no implementa una función de bloqueo vtable y la implementación de la clase base (FSRegObject ) es simplemente una instrucción nop. Esto significa que 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 su limpieza, en la función FSRendezvousServer::Close :

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

Bloque de código 1: FSRendezvousServer::Cerrar, bloquear y desbloquear la ruta para FSRegObjects

Hay dos objetos derivados de la clave 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í, me di cuenta de que había algo “sospechoso” en el sistema de recuento de referencias para FSRegObjects  . Y aún no sabía qué.

IPC en el servidor MS KS

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 del mecanismo de IPC del subsistema

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 argumentolpInBuffer . El proceso remoto, usando un nuevo identificador de archivo hacia el dispositivo MSKSSRV, puede ahora registrar el objeto a través del IOCTL del dispositivo. El puntero del 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 una vez en el FILE_OBJECT utilizado por el proceso de registro. De esta manera, las referencias al objeto se pueden compartir entre procesos FSRegObject . 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 FSRegObject se inicializa en 1 y, a continuación, se incrementa de nuevo una vez completada la inicialización. El registro de FSRegObject aumenta de nuevo su recuento de referencias, hasta 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 bug

Decidí echar otro vistazo a lo que había auditado anteriormente para buscar vulnerabilidades de bloqueo. He observado que la función FSRendezvousClose, que llama a FSRendezvousServer::Close , se llama dentro de las rutinas de limpieza y cierre de la función de envío del controlador:

Captura de pantalla de un bloque de código con rutinas DispatchCleanup y DipatchCleanup para MS KS Server Driver

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 IRP_MJ_CLEANUP  y una solicitud IRP_MJ_CLOSE  , que se gestionan mediante las rutinas de funciones DispatchCleanup y DispatchClose del controlador.

En MSKSSRV, se llama aFSRendezvousClose  dentro de las rutinas de función del controlador DispatchCleanup y DispatchClose, si el puntero almacenado enIrp->CurrentStackLocation->FileObject->FsContext2 no es NULL. Algo que me llamó la atención en FSRendezvousServer::Close , que había analizado anteriormente, eran las diversas comprobaciones del ID del proceso de la persona que llamaba. 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 se separa 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 en modo usuario del proceso se accederá si el hilo accede a direcciones de usuario. El siguiente código comprueba si el proceso que llama 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 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 DispatchCleanup  se llama en el contexto del proceso del proceso que cerró el identificador final. Así que, en este caso, las comprobaciones de identificación del proceso tienen sentido. Sin embargo, DispatchClose se llama desde un contexto de proceso arbitrario. Eso significa que si FSRendezvousServer::Close se llama como resultado de la solicitudIRP_MJ_CLOSE , provendría de un contexto de proceso arbitrario, lo que iría en contra del propósito de las comprobaciones de ID del proceso. Esta fue una gran pista de que algo andaba mal aquí.

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 FILE_OBJECT .

Diagrama de uso compartido del identificador del dispositivo del servidor MS KS

 Diagrama 3: Compartir el identificador del dispositivo del servidor MS KS

Por esto, también es posible que el contexto del proceso durante DispatchCleanup  no es 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á aDispatchCleanup  dentro del contexto de ese proceso extranjero (que no es el proceso de inicialización o registro).

Diagrama del proceso externo que cierra el último identificador a un FILE_OBJECT

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 FSRendezvousServer::Close  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 desreferencian el objeto y liberan su memoria si el recuento de referencias cae a 0. Eso significa que se podría llamar FSRegObject::Release  hasta cuatro veces para un identificador. Además, hay dos identificadores de archivo para los que elFILE_OBJECT  apunta al mismo FSRegObject a través del puntero FSContext2 . Eso significa que es posible 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 producirse un posible uso después de la liberación provocado por demasiadas desreferencias. En cualquier caso, ese era mi razonamiento. Sabía que la estructura del programa probablemente evitaría alcanzar el número máximo teórico de desreferencias, 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 FSRegObject::Release puede llamarse dos veces desde FSRendezvousServer::Close

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 FSRegObject su recuento de referencias se ha reducido a cero. La última vez que se accede a un válido FSRegObject 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, 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 IRP_MJ_CLEANUP/CLOSE , 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 FileObject->FSContext2 apunta a la misma memoria en amosb FILE_OBJECT representados por los identificadores correspondientes.

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

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 objetosFSStreamReg  , pero debido a un error de pérdida de memoria en el código, en realidad no se puede liberar un objeto FSStreamReg  después de registrarse.

La vulnerabilidad

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 Irp->CurrentStackLocation->FileObject->FsContext2 se establece en NULL . El bloque de código siguiente muestra dónde ocurre esto en el caso en que el llamador sea el proceso de inicialización:

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

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

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

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

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 FSRegObject::Release  dentro de FSRendezvousServer::Close (Bloque de código 4). La desreferencia adicional sirve para tener en cuenta la referencia de inicialización adicional una vez que el objeto está vacío.

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

Diagrama que muestra el CVE-2024-30089

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 . Al comienzo 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 a FSRegObject::Release al final de la función. Durante la cuarta llamada al 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 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 FSRendezvousServer::Close . El fragmento de código siguiente muestra la primitiva 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 UAF

Complejidad del ataque

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.

Una retrospectiva

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 FSRendezvousServer::Close .

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

Irp->CurrentStackLocation->FileObject->FsContext2

 

NULL

 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:

Captura de pantalla del código de la versión anterior de FSRendezvousServer::Close, FsContext2 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  está establecido explícitamente en NULL , independientemente del resultado de las comprobaciones de identidad del proceso anterior. Esto evita que se vuelva a llamar a FSRendezvousServer::Close  en la siguiente solicitud IRP_MJ_CLOSE  , por lo que no se puede producir la desreferencia adicional. Qué raro... ¿por qué se eliminó esta línea de código? Echemos un vistazo a una versión posterior de la función, en la que se introdujo el error por primera vez:

Captura de pantalla de FsContext establecido en NULL dentro de una comprobación de indicador de característica

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, FileObject->FsContext2 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 NULL , 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.

El parche

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 FSRendezvousServer::InitializeContext :

Captura de pantalla de la función IOCTL que comienza con una característica y comprueba si el proceso de llamada es un servidor de marcos

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, KsIsCurrentProcessFrameServer debe devolver TRUE, de lo contrario NTSTATUS  el valor STATUS_ACCESS_DENIED   se devuelve.

Echemos un vistazo a KsIsCurrentProcessFrameServer :

Captura de pantalla de KsIsCurrentProcessFrameServer realizando una comprobación SID en el token de acceso del hilo que llama

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 NT SERVICE\FrameServer . 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 FrameServer , 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.

Conclusión y próximos pasos

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

Agradecimientos

Andréa Piazza, por los increíbles diagramas

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

Mixture of Experts | 12 de diciembre, episodio 85

Descifrar 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 bullicio de la IA para ofrecerle las últimas noticias y conocimientos al respecto.