Críticamente cerca de día cero: explotación del servicio de transmisión del kernel de Microsoft.

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

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.

La superficie de ataque

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:

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

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.

Las últimas noticias tecnológicas, respaldadas por los insights de expertos

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.

¡Gracias! Ya está suscrito.

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.

Análisis inicial

Activación de la ejecución dentro de MS KS Server

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

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

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

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.

Depuración de un controlador fantasma

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.

Comentar en un foro

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.

Una encuesta rápida 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 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.

La vulnerabilidad

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

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.

Explotación

Primitiva

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

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

Rociado de piscina

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:

Ilustración de rociado de piscina no paginado

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.

Escritura constante

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

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

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.

El problema de la carga

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

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.

Fuga en la piscina: ¡no rocíe y ore!

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

FSRendezvousServer::ConsumeTx decompilation snippet

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

Descompilación de FSStreamReg::GetStats

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

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.

En la explotación salvaje

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

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

Mixture of Experts | 12 de diciembre, episodio 85

Decodificación de 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 revuelo de la IA para ofrecerle las últimas noticias e insights al respecto.