El mes pasado, Microsoft corrigió una vulnerabilidad en Microsoft Kernel Streaming Server, un componente del kernel 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 SISTEMA.
Esta entrada en el blog detalla mi proceso de explorar una nueva superficie de ataque en el kernel de Windows, encontrar una vulnerabilidad de día 0, explorar una clase de error interesante y construir un exploit estable. No es necesario tener conocimientos especializados sobre el núcleo de Windows para seguir este artículo, aunque resulta útil tener 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 kernel desconocido y simplificaré el proceso de buscar un nuevo objetivo.
Microsoft Kernel Streaming Server (mskssrv.sys) es un componente de un servicio de Windows infraestructura Multimedia, 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 CVE-2023-29360, que inicialmente figuraba como una vulnerabilidad del controlador TPM. El error está en realidad en el Microsoft Kernel Streaming Server. Aunque en ese momento no estaba familiarizado con MS KS Server, 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 kernel podría ser un lugar fructífero para buscar vulnerabilidades. Al entrar a ciegas, busqué responder a las siguientes preguntas:
Para responder a la primera pregunta, comencé analizando el binario en un desensamblador. Rápidamente identifiqué la vulnerabilidad mencionada, un simple y elegante error lógico. El problema parecía sencillo de activar y explotar por completo, por lo que busqué desarrollar una prueba de concepto rápida para comprender mejor el funcionamiento interno del controlador mskssrv.sys.
Boletín de la industria
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.
Su suscripción se entregará en inglés. En cada boletín, encontrará un enlace para darse de baja. Puede gestionar sus suscripciones o darse de baja aquí. Consulte nuestra Declaración de privacidad de IBM para obtener más información.
Primero, 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 alcanzar emitiendo un IOCTL al controlador. Para ello, es necesario obtener un identificador del dispositivo del controlador mediante una llamada a CreateFile utilizando la ruta del dispositivo. Normalmente, encontrar el nombre/ruta del dispositivo es sencillo de identificar: 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
En este caso, el parámetro para el 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 de 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 I/O a un dispositivo. Para dispositivos PnP, se necesita la ruta de interfaz del dispositivo para acceder al dispositivo.
Con el depurador del núcleo WinDbg podemos ver qué dispositivos pertenecen al controlador mskssrv y a la pila de dispositivos:
Salida de los comandos !drvobj y !devobj que muestran los dispositivos superior e inferior
Arriba vemos que el dispositivo de mskssrv está conectado al objeto de dispositivo inferior que pertenece al controlador swenum.sys y tiene un dispositivo superior conectado que pertenece a ksthunk.sys.
Desde el Administrador de dispositivos podemos encontrar el ID de la instancia del dispositivo de destino:
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 administrador de configuración o las funciones SetupApi. Utilizando la ruta de interfaz del dispositivo recuperada, podemos abrir un identificador para el dispositivo.
Finalmente, 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 creación de envío PnP del controlador. Para activar la ejecución de código adicional, podemos enviar IOCTL para comunicarnos con el dispositivo, que se ejecutará en la función de control del dispositivo de despacho del controlador.
Al realizar análisis binarios, una buena práctica es utilizar una combinación de herramientas estáticas (desensamblador, decompilador) y dinámicas (depurador). WinDbg se puede utilizar para depurar el kernel del controlador de destino. Estableciendo algunos puntos de interrupción en los lugares donde se espera que se ejecute el código (creación de envíos, control de dispositivos de envío).
Al principio tuve algunas dificultades: ninguno de los puntos de interrupción que establecí dentro del controlador se activaba. Tenía algunas dudas de si estaba abriendo el dispositivo correcto o si estaba haciendo algo mal. Más tarde me di cuenta de que mis puntos de ruptura se estaban desconfigurando 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.
Resulta que los controladores de filtro PnP se pueden descargar si no se han utilizado durante un tiempo y volver a cargar bajo demanda cuando sea necesario.
Solucioné los problemas que tenía configurando puntos de interrupción después de que se abriera una manilla del dispositivo, pero antes de llamar a DeviceIoControl, para cerciorarme de que el controlador se instaló recientemente.
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:
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 este punto, me adentré más en la funcionalidad prevista para el controlador. Encontré esta presentación de Michael Maltsev sobre la infraestructura multimedia de Windows, donde descubrí que el controlador es parte de un mecanismo entre procesos para compartir transmisiones de cámara.
Como el controlador no es muy grande y no hay muchos IOCTL, podrí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 un objeto de registro de flujo, que se asigna e inicializa a través de sus correspondientes IOCTL “Inicializar”. El puntero al objeto se almacena Irp- > CurrentStackLocation- > FileObject- > fsContext2. FileObject apunta al objeto de archivo de dispositivo creado por archivo abierto, y FSContext2 es un campo diseñado para almacenar metadatos por objeto de archivo.
Detecté este error mientras intentaba comprender cómo comunicarme directamente con el controlador, dejando de lado en primer lugar el análisis de los componentes del modo de usuario, fsclient.dll. y frameserver.dll. Casi me perdí el error, porque asumí que los desarrolladores instanciaron una simple verificación que se pasó por alto. Echemos un vistazo a la función IOCTL de PublishRx:
Fragmento de descompilación de FSRendezvousServer::PublishRx
Luego de recuperar el objeto de flujo de FsContext2, se llama a la función FSRendezvousServer::FindObject para Verify que el puntero coincida con un objeto encontrado en dos listas almacenar por el FSRendezvousServer global. Al principio, asumí que esta función tendría alguna forma de Verify el tipo de objeto aplicar. Sin embargo, la función devuelve TRUE si el puntero se encuentra en la lista de objetos de contexto o en la lista de objetos de flujo. Observe que no se pasa a FindObjectque se supone que debe ser el objeto a FindObject. Eso significa que un objeto de contexto se puede pasar como un objeto de flujo. ¡Esta es una vulnerabilidad de confusión de tipos de objeto! Ocurre en todas las funciones IOCTL que operan en objetos de flujo. Para corregir la vulnerabilidad, Microsoft reemplazó FSRendezvousServer::FindObject con FSRendezvousServer::FindStreamObject, que primero verifica que el objeto sea un objeto de flujo comprobando un campo de tipo.
Debido a 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 los límites:
Ilustración de vulnerabilidad de confusión de tipo de objeto
Para aprovechar la vulnerabilidad primitiva, necesitamos poder 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 rociado en montón o piscina. El objeto vulnerable se asigna en un grupo de montones de fragmentación baja no paginado. Podemos usar la técnica tradicional de Alex Ionescu para pulverizar buffers que dan control total del contenido de memoria por debajo de una cabecera DATA_QUEUE_ENTRY 0x30 byte. Al rociar con esta técnica, podemos obtener el diseño de memoria que se muestra en el diagrama:
Usando el método elegido de rociado de piscinas, los campos en compensaciones de objetos dentro de los rangos 0xC0-0x10F y 0x150-0x19F pueden ser controlados. Una vez más volví a visitar las funciones de IOCTL para objetos de transmisión para buscar primitivas de explotar. Busqué lugares en los que se accede y manipula los campos de objetos controlables.
Encontré una buena primitiva de escritura constante en el IOCTL de PublishRx. Esta primitiva se puede utilizar 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 FSStreamReg::PublishRx
El objeto stream contiene un encabezado de lista en el desfase 0x188 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 .
Utilizando la primitiva de explotar mencionada anteriormente, se puede controlar totalmente la FSFrameMdlList y, por lo tanto, el objeto FsFrameMdl al que apunta pFrameMdl. Veamos ahora UnmapPages:
Descompilación de FSFrameMdl:UnmapPages
En la última línea de la función descompilada anterior, el valor constante 2 se escribe en un valor de desplazamiento de este ( objeto FSFrameMdl ) que es controlable. Esta escritura constante se puede utilizar junto con la técnica I/O Ring para obtener una lectura y escritura arbitrarias del kernel 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, también aparece otra primitiva de explotar útil en esta función. Ambos argumentos BaseAddress y MemoryDescriptorList para la llamada a MmUnmapLockedPages son controlables. Esto podría usarse para desbaratar un mapeo en una dirección virtual arbitraria y construir una primitiva uso de memoria liberada.
En este punto, se han identificado varias primitivas de explotar adecuadas que dan lectura-escritura arbitraria del kernel. Es posible que haya notado que hay varias comprobaciones en el contenido del objeto de flujo que se deben pasar para activar la ruta de código deseada. En su mayor parte, el estado adecuado del objeto se puede lograr mediante el rociado de la piscina. Sin embargo, encontré un problema que causó cierta dificultad. A continuación se muestra un fragmento de código de FSStreamReg::PublishRx después de terminar de recorrer FSFrameMdlList:
Fragmento de descompilación 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.
Este desplazamiento corresponde a la memoria fuera de los límites y apunta dentro de un POOL_HEADER, la estructura de datos que separa las asignaciones de búfer en el grupo. En concreto, apunta al campo ProcessBilled, que se utiliza para almacenar un puntero al objeto _EPROCESS para el proceso que se «encarga» de la asignación. Se utiliza para contabilizar cuántas asignaciones de grupo puede tener un proceso concreto. No todas las asignaciones de pool se “cobran” contra un proceso, y aquellas que no tienen el campo ProcessBilled establecido en NULL en POOL_HEADER. Además, el puntero EPROCESS almacenado en ProcessBilled en realidad se hace XOR con una cookie aleatoria, por lo que ProcessBilled no contiene un puntero válido.
Esto presenta una dificultad, porque los buffers NpFr se cargan al proceso que llama y, por lo tanto, se establece ProcessBilled. Al activar la primitiva de explotar 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 sin cargo. En este punto, me di cuenta de que el objeto de registro de contexto (Creg) en sí mismo 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 los Creg deben pulverizarse y secuenciarse correctamente.
A diferencia de las asignaciones de memoria grandes, no se pueden filtrar las direcciones de las asignaciones de memoria 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 buffers adyacentes al objeto vulnerable están en el orden correcto para explotar la primitiva y evitar que el sistema se bloquee. Afortunadamente, la vulnerabilidad se puede utilizar para provocar una fuga de memoria en los búferes adyacentes. Echemos un vistazo a la función IOCTL para ConsumeTx:
FSRendezvousServer::ConsumeTx decompilation snippet
Arriba, la función FSStreamReg::GetStats se llama:
Descompilación de FSStreamReg::GetStats
Aquí, el contenido de 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 del grupo se puede utilizar 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 del diseño de memoria deseado. Una vez localizado el objeto deseado, la disposición de la memoria es la siguiente:
CVE-2023-36802 Diseño de mantenimiento de grupo de pila de baja fragmentación
Ahora, habiendo localizado el objeto vulnerable de destino en la posición correcta en la memoria, la primitiva de explotar mencionada anteriormente en el objeto objetivo se puede activar sin que el sistema se estrelle.
Después de informar el problema a MSRC, se descubrió la explotación salvaje de la vulnerabilidad.
Los métodos de explotación presentados en esta entrada en el blog son algunos de los muchos enfoques que se podrían tomar. Actualmente, no hay información pública sobre cómo los atacantes en estado salvaje explotaron esta vulnerabilidad. Puedes encontrar el código de explotar aquí.
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. El monitoreo de las nuevas incorporaciones de código suele ser útil para encontrar vulnerabilidades.
Otra lección cansada, pero clásica, que se puede aprender de este análisis: no haga suposiciones sobre las comprobaciones realizadas. Un amigo y colega sugirió que la confusión de tipos con FsContext2 podría ser una "clase de error común pero poco investigada". Creo que se justifica más análisis de variantes para esta clase de error, particularmente en 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 “conocimiento críticamente cercano a cero” de un sistema también puede significar tener la mentalidad fresca para romperlo.