Críticamente cerca de (día) cero: explotación del servicio de streaming de Microsoft Kernel

Foto de un hombre con barba trabajando hasta tarde en la oficina para cumplir su fecha límite, con dos compañeros de trabajo sentados detrás de él

El mes pasado, Microsoft parcheó una vulnerabilidad en el servidor Microsoft Kernel Streaming, un componente del núcleo de Windows utilizado en la virtualización y el uso compartido de dispositivos de cámara. La vulnerabilidad, CVE-2023-36802, permite a un atacante local escalar privilegios a SYSTEM.

Esta entrada de blog detalla mi proceso de explorar una nueva superficie de ataque en el núcleo de Windows, encontrar una vulnerabilidad de día 0, explorar una clase de error interesante y construir un exploit estable. Esta publicación no requiere ningún conocimiento especializado del núcleo de Windows para seguirla, aunque es útil una comprensión básica de la corrupción de memoria y los conceptos del sistema operativo. También cubriré los conceptos básicos para realizar el análisis inicial en un controlador de núcleo desconocido y simplificaré el proceso de buscar un nuevo objetivo.

La superficie de ataque

El servidor Microsoft Kernel Streaming (mskssrv.sys) es un componente de un servicio de Windows Multimedia Framework, Frame Server. El servicio virtualiza el dispositivo de la cámara y permite compartirlo entre varias aplicaciones.

Comencé a explorar esta superficie de ataque después de observar la CVE-2023-29360, que inicialmente fue listada como una vulnerabilidad del controlador TPM. De hecho, el error está en el servidor de Microsoft Kernel Streaming. Aunque en ese momento no conocía el servidor MS KS, el nombre de este controlador fue suficiente para mantener mi interés. A pesar de no saber nada sobre su propósito o funcionalidad, pensé que un servidor de streaming en el núcleo podría ser un lugar fructífero para buscar vulnerabilidades. Al entrar a ciegas, buscaba responder a las siguientes preguntas:

  • ¿En qué medida puede una aplicación sin privilegios interactuar con este módulo del núcleo?
  • ¿Qué tipo de datos de la aplicación procesa directamente el módulo?

Para responder a la primera pregunta, empecé analizando el binario en un desensamblador. Rápidamente identifiqué la vulnerabilidad mencionada, un simple y elegante error lógico. El problema parecía fácil de activar y explotar, así que me propuse desarrollar una prueba de concepto rápida para entender mejor el funcionamiento interno del controlador mskssrv.sys.

Las últimas novedades sobre tecnología, respaldadas por conocimientos de expertos

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.

¡Gracias! Se ha suscrito.

Su suscripción se enviará en inglés. Encontrará un enlace para darse de baja en cada boletín. Puede gestionar sus suscripciones o darse de baja aquí. Consulte nuestra Declaración de privacidad de IBM para obtener más información.

Análisis inicial

Activación de la ejecución dentro del servidor MS KS

En primer lugar, necesitamos poder llegar al controlador desde una aplicación de espacio de usuario. La función vulnerable es accesible desde la rutina DispatchDeviceControl del controlador, lo que significa que puede alcanzarse emitiendo un IOCTL al controlador. Para hacerlo, se debe obtener un identificador del dispositivo del controlador mediante una llamada a CreateFile utilizando la ruta del dispositivo. Normalmente, encontrar e identificar el nombre/ruta del dispositivo es sencillo: busque una llamada a IoCreateDevice en el controlador y examine el tercer parámetro que contiene el nombre del dispositivo.

Función dentro de mskssrv.sys que llama a IoCreateDevice con un puntero NULL para el nombre del dispositivo

Función dentro de mskssrv.sys que llama a IoCreateDevice con un puntero NULL para el nombre del dispositivo

En este caso, el parámetro del nombre del dispositivo es NULL. El nombre de la función de llamada sugiere que mskssrv es un controlador PnP , y la llamada a  IoAttachDeviceToDeviceStack indica que el objeto dispositivo creado forma parte de una pila de dispositivos. En efecto, esto significa que se llama a varios controladores cuando se envía una solicitud de E/S a un dispositivo. Para dispositivos PnP, se necesita la ruta de interfaz del dispositivo para acceder al dispositivo.

Utilizando el depurador del núcleo WinDbg podemos ver qué dispositivos pertenecen al controlador mskssrv y a la pila de dispositivos:

Output del comando !drvobj y !devobj que muestra los dispositivos superiores e inferiores

Output del comando !drvobj y !devobj que muestra los dispositivos superiores e inferiores

Arriba vemos que el dispositivo de mskssrv está conectado al objeto de dispositivo inferior perteneciente al controlador swenum.sys y tiene un dispositivo superior adjunto que pertenece a ksthunk.sys.

Desde el Administrador de dispositivos podemos encontrar la ID de instancia del dispositivo deobjetivo:

Administrador de dispositivos mostrando el ID de instancia del dispositivo y el GUID de la interfaz

Administrador de dispositivos mostrando el ID de instancia del dispositivo y el GUID de la interfaz

Ahora tenemos suficiente información para obtener la ruta de la interfaz del dispositivo utilizando el gestor de configuración o las funciones SetupApi. Usando la ruta de la interfaz del dispositivo recuperada, podemos abrir un handle identificador al dispositivo.

Por último, ahora podemos activar la ejecución de código dentro de mskssrv.sys. Cuando se crea el dispositivo, se llama a la función de dispatch createde PnP del controlador. Para desencadenar la ejecución de código adicional, podemos enviar IOCTLs para hablar con el dispositivo que se ejecutarán en la función dispatch device control del controlador.

Depuración de un controlador fantasma

Al realizar análisis binarios, es una buena práctica utilizar una combinación de herramientas estáticas (desensamblador, descompilador) y dinámicas (depuradoras). WinDbg puede usarse para depurar el núcleo del controlador objetivo. Al establecer algunos puntos de interrupción en los lugares en los que se espera que se produzca la ejecución del código (creación de envío, control de dispositivo de envío).

Al principio tuve algunas dificultades: no se alcanzaba ninguno de los puntos de ruptura que fijé dentro del controlador. Tenía algunas dudas sobre si estaba abriendo el dispositivo correcto o si estaba haciendo algo mal. Después me di cuenta de que mis puntos de quiebre no estaban configurados porque se estaba descargando el controlador. Busqué respuestas en Internet, sin embargo, no hay muchos resultados al buscar mskssrv, a pesar de estar cargado y accesible de forma predeterminada en Windows. Entre los pocos resultados que encontré, había un hilo en OSR , donde alguien más encontró un problema similar.

Comentar en un foro

Resulta que los controladores de filtro PnP pueden descargarse si no se han utilizado durante un tiempo y volver a cargarse a demanda cuando sea necesario.

Resolví los problemas que tenía configurando puntos de interrupción después de abrir el identificador del dispositivo, pero antes de llamar a DeviceIoControl, para asegurarme de que el controlador se había cargado recientemente.

Una breve encuesta de funcionalidad del controlador

El controlador mskssrv es un binario de solo 72 KB y admite códigos de control de E/S de dispositivo que llaman a las siguientes funciones:

  • FSRendezvousServer::InitializeContext
  • FSRendezvousServer::InitializeStream
  • FSRendezvousServer::RegisterContext
  • FSRendezvousServer::RegisterStream
  • FSRendezvousServer::DrainTx
  • FSRendezvousServer::NotifyContext
  • FSRendezvousServer::PublishTx
  • FSRendezvousServer::PublishRx
  • FSRendezvousServer::ConsumeTx
  • FSRendezvousServer::ConsumeRx

Al observar estos nombres de símbolos podemos inferir alguna funcionalidad del controlador, algo relacionado con la transmisión y recepción de flujos. En ese momento, me adentré más en la funcionalidad prevista por el controlador. Encontré esta presentación de Michael Maltsev sobre el marco multimedia de Windows en la que deduje que el controlador forma parte de un mecanismo entre procesos para compartir flujos de cámaras.

Como el controlador no es muy grande y no hay muchos IOCTLs, podía mirar cada función para tener una idea de los internos del controlador. Cada función IOCTL opera en un objeto de registro de contexto o en un objeto de registro de flujo, que se asigna e inicializa a través de sus IOCTL "Initialize" correspondientes. El puntero al objeto está almacenado Irp- > CurrentStackLocation- > FileObject- > fsContext2. FileObject apunta al objeto de archivo del dispositivo creado por archivo abierto y FSContext2 es un campo destinado a almacenar los metadatos de cada objeto de archivo.

La vulnerabilidad

Descubrí este error cuando intentaba entender cómo comunicarse directamente con el controlador, primero antes del análisis de los componentes del modo de usuario, fsclient.dll y frameserver.dll. Estuve a punto de pasar por alto el fallo, porque supuse que los desarrolladores habían instanciado una simple comprobación que se pasó por alto. Echemos un vistazo a la función IOCTL de PublishRx:

Fragmento de descompilación de FSRendezvousServer::PublishRx

Fragmento de descompilación de FSRendezvousServer::PublishRx

Después de recuperar el objeto de flujo de FsContext2, se llama a la función FSRendezvousServer::FindObject, para verificar que el puntero coincide con un objeto encontrado en dos listas almacenadas por el FSRendezvousServer global. Al principio, supuse que esta función tendría alguna forma de verificar el tipo de objeto solicitado. Sin embargo, la función devuelve TRUE si el puntero se encuentra tanto enla lista de objetos de contexto como en la lista de objetos de flujo. Observe que ninguna información sobre qué tipo de objeto se supone que es se pasa a FindObject . Esto significa que un objeto de contexto puede pasarse como un objeto de flujo. ¡Esto es una vulnerabilidad de confusión de tipo de objeto! Se produce en todas las funciones IOCTL que operan con objetos de flujo. Para parchear la vulnerabilidad, Microsoft sustituyó FSRendezvousServer::FindObject por FSRendezvousServer::FindStreamObject, que primero verifica que el objeto es un objeto de flujo comprobando un campo de tipo.

Explotación

Primitivo

Dado que los objetos de registro de contexto son más pequeños (0x78 bytes) que los objetos de registro de flujo (0x1D8 bytes), las operaciones de objetos de flujo se pueden realizar en memoria fuera de límites:

Ilustración de vulnerabilidad de confusión de tipo de objeto

Ilustración de vulnerabilidad de confusión de tipo de objeto

Pulverizado de agrupación

Para aprovechar la vulnerabilidad primitiva, necesitamos la capacidad de controlar la memoria fuera de límites a la que se accede. Esto se puede hacer activando la asignación de muchos objetos en la misma área de memoria del objeto vulnerable. Esta técnica se llama pulverizado de montón o agrupación. El objeto vulnerable se asigna en una agrupación de montón no paginado de baja fragmentación. Podemos usar la técnica clásica de Alex Ionescu para pulverizar búfers que dan control total del contenido de memoria por debajo de una cabecera DATA_QUEUE_ENTRY 0x30 byte. Al pulverizar con esta técnica, podemos obtener el diseño de memoria que se muestra en el diagrama:

Ilustración de pulverizado de agrupación no paginada

Utilizando el método elegido de pulverizado de agrupación, los campos en los desplazamientos de objetos dentro de los rangos 0xC0-0x10F y 0x150-0x19F se pueden controlar. Volví a revisar las funciones IOCTL para objetos de flujo en busca de primitivas de explotación. Busqué los lugares en los que se accede y manipulan los campos de objeto controlables.

Escritura constante

Encontré una buena primitiva de escritura constante en la IOCTL de PublishRx. Esta primitiva puede utilizarse para escribir un valor constante en una dirección de memoria arbitraria. Echemos un vistazo a un fragmento de la función fsStreamReg: :PublishRx:

Fragmento de descompilación de FSStreamReg::PublishRx

Fragmento de descompilación de FSStreamReg::PublishRx

El objeto de flujo contiene una cabeza de lista en 0x188 desplazada que describe una lista de objetos FSFrameMdl . En el fragmento de descompilación anterior, esta lista se itera y si el valor de la etiqueta en el objeto FSFrameMdl coincide con la etiqueta en el búfer del sistema pasado desde la aplicación, se llama a la función FSFrameMdl::UnmapPages .

Usando la primitiva de explotación mencionada, se puede controlar completamente la FSFrameMdlList y, por tanto, también el objeto FsFrameMdl al que apunta pFrameMdl . Veamos ahora UnmapPages:

Descompilación de FSFrameMdl:UnmapPages

Descompilación de FSFrameMdl:UnmapPages

En la última línea de la función descompilada anterior, el valor constante 2 se está escribiendo en un valor de desplazamiento de este ( objetoFSFrameMdl ) que es controlable. Esta escritura constante puede utilizarse junto con la técnica del anillo de E/S para obtener una lectura-escritura arbitraria del núcleo y una escalada de privilegios. Puedes leer más sobre cómo funciona esta técnica aquí y aquí.

Aunque elegí utilizar la primitiva de escritura constante, en esta función aparece otra primitiva de explotación útil. Ambos argumentos BaseAddress y MemoryDescriptorList para la llamada a MmUnmapLockedPages son controlables. Esto podría usarse para desmapear un mapeo en una dirección virtual arbitraria y construir una primitiva de tipo use-after-free .

El problema de la carga

En este punto, se han identificado varias primitivas de explotación adecuadas que proporcionan lectura-escritura arbitraria del núcleo. Quizá hayas notado que hay varias comprobaciones en el contenido del objeto de flujo que deben pasarse para activar la ruta de código deseada. En la mayoría de los casos, el estado adecuado del objeto puede conseguirse mediante la pulverización de agrupación. Sin embargo, me encontré con un problema que causó algunas dificultades. A continuación se muestra un fragmento de código de FSStreamReg::PublishRx después de que termine de recorrer la FSFrameMdlList:

Fragmento de descompilación de FSStreamReg::PublishRx

Fragmento de descompilación de FSStreamReg::PublishRx

En la descompilación anterior, bPagesUnmapped es una variable booleana que se establece si se llama a FSFrameMdl::UnmapPages. Si es así, se recupera el desplazamiento 0x1a8 del objeto de flujo y, si no es nulo, se llama aKeSetEvent en él.

Este desplazamiento corresponde a la memoria fuera de límites y apunta dentro de un POOL_HEADER, la estructura de datos que separa las asignaciones de búfer en la agrupación. En particular, apunta al campo ProcessBilled , que se utiliza para almacenar un puntero al objeto _EPROCESS para el proceso que está “cargado” con la asignación. Esto se utiliza para calcular cuántas asignaciones de agrupación puede tener un proceso en particular. No todas las asignaciones de agrupación se "cargan" en un proceso, y aquellas que no tienen el campo  ProcessBilled establecido en NULL en POOL_HEADER. Además, el puntero EPROCESS almacenado en ProcessBilled es en realidad XOR con una cookie aleatoria, por lo que ProcessBilled no contiene un puntero válido.

Esto presenta una dificultad, porque los búfers NpFr se cargan al proceso que llama y, por lo tanto, se establece ProcessBilled . Al activar la primitiva de explotación necesaria, bPagesUnmapped se configurará en TRUE. Si se pasa un puntero no válido a KeSetEvent, el sistema se bloqueará. Por lo tanto, es necesario asegurarse de que POOL_HEADER sea para una asignación no cargada. En ese momento, me di cuenta de que el objeto de registro de contexto (Creg) en sí no se carga. Sin embargo, este objeto no permite controlar el contenido de la memoria en el desplazamiento FSFrameMdl. Por lo tanto, tanto los objetos NpFr como losCreg necesitan ser pulverizados y también necesitan ser secuenciados correctamente.

Fuga en la agrupación: ¡Nada de pulverizar y rezar!

A diferencia de las grandes asignaciones de agrupación, no se pueden filtrar las direcciones de las asignaciones de agrupación LFH a través de NtQuerySystemInformation. Además, el orden de asignación es aleatorio. Por lo tanto, no hay forma de saber si los búferes adyacentes al objeto vulnerable están en el orden correcto para activar la primitiva de explotación y evitar que se bloquee el sistema. Afortunadamente, la vulnerabilidad puede utilizarse para desencadenar una fuga de agrupación de los búferes adyacentes. Echemos un vistazo a la función IOCTL para ConsumeTx:

FSRendezvousServer::ConsumeTx decompilation snippet

FSRendezvousServer::ConsumeTx decompilation snippet

Arriba, la función FSStreamReg::GetStats se llama:

Descompilación de FSStreamReg::GetStats

Descompilación de FSStreamReg::GetStats

En este caso, el contenido de la memoria fuera de límites del objeto de flujo vulnerable se copia en el SystemBuffer , que se devuelve a la aplicación de espacio de usuario que llama. Esta primitiva de fuga de información de agrupación puede utilizarse para realizar una comprobación de firma en los búferes adyacentes al objeto vulnerable. Se puede realizar un escaneo de muchos objetos vulnerables hasta que se encuentre el objeto dentro de la distribución de memoria deseada. Una vez localizado el objeto deseado, la disposición de la memoria es la siguiente:

CVE-2023-36802 Distribución de agrupación de montón de Baja Fragmentación

CVE-2023-36802 Distribución de agrupación de montón de Baja Fragmentación

Ahora, una vez localizado el objeto vulnerable objetivo en la posición correcta de la memoria, la primitiva de explotación antes mencionada en el objeto objetivo se puede activar sin que se bloquee el sistema.

Explotación in-the-wild

Tras informar del problema al MSRC, se descubrió la explotación in-the-wild de la vulnerabilidad.

Los métodos de explotación presentados en esta entrada de blog son algunos de los muchos enfoques que podrían adoptarse. Actualmente, no hay información pública sobre cómo los atacantes in-the-wild explotaron esta vulnerabilidad. Puede encontrar el código del exploit aquí.

Conclusión

El análisis retroactivo de parches reveló que una gran parte del código nuevo se agregó a mskssrv.sys en la compilación 1809 de Windows 10. La monitorización de nuevas adiciones de código suele ser fructífera para encontrar vulnerabilidades.

Otra lección cansada, pero clásica, que se puede aprender de este análisis es que no haga suposiciones sobre las comprobaciones realizadas. Un amigo y colega sugirió que la confusión de tipos que usan FsContext2 podría ser una clase de error común pero poco investigada. Creo que justifica un mayor análisis de variantes para esta clase de fallos, en particular en los controladores que se ocupan de la comunicación entre procesos.

El descubrimiento de esta vulnerabilidad se produjo simplemente al intentar interactuar con una superficie de ataque desconocida. Tener un "conocimiento críticamente cercano a cero" de un sistema también puede significar tener la mentalidad fresca para romperlo.

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.