"Martes de parches, miércoles de exploits" es un viejo dicho hacker que se refiere al uso de vulnerabilidades como armas el día después de que los parches de seguridad mensuales estén disponibles públicamente. A medida que la seguridad mejora y las mitigaciones de exploits se vuelven más sofisticadas, la cantidad de investigación y desarrollo requerida para crear un arma de exploit ha aumentado. Esto es especialmente relevante para las vulnerabilidades de corrupción de memoria.
Figura 1: Cronología de la explotación
Sin embargo, con la incorporación de nuevas características (y código C inseguro para la memoria) en el núcleo de Windows 11, pueden aparecer nuevas superficies de ataque. Al perfeccionar este código recién introducido, demostramos que las vulnerabilidades que pueden convertirse en armas de manera trivial siguen ocurriendo con frecuencia. En esta entrada en el blog, analizamos y explotamos una vulnerabilidad en el controlador de funciones auxiliares de Windows para Winsock, afd.sys, para la escalada de privilegios locales (LPE) en Windows 11. Aunque ninguno de nosotros tenía experiencia con este módulo del kernel, pudimos diagnosticar, reproducir y convertir la vulnerabilidad en un arma en aproximadamente un día. Puede encontrar el código de exploit aquí.
Según los detalles de CVE-2023-21768 publicados por el Microsoft Security Response Center (MSRC), la vulnerabilidad se encuentra en el controlador de funciones auxiliares (AFD), cuyo nombre de archivo binario es afd.sys. El módulo AFD es el punto de entrada del kernel para la API Winsock. Con esta información, analizamos la versión del controlador de diciembre de 2022 y la comparamos con la versión recién lanzada en enero de 2023. Estas muestras pueden obtenerse individualmente desde Winbindex sin el proceso lento de extraer cambios de los parches de Microsoft. Las dos versiones analizadas se muestran a continuación.
Se utilizó Ghidra para crear exportaciones binarias para ambos archivos para que pudieran compararse en BinDiff. A continuación, se muestra una descripción general de las funciones coincidentes.
Figura 2: Comparación binaria de AFD.sys
Solo una función parecía haber cambiado,
Antes del parche,
Figura 3: afd!AfdNotifyRemoveIoCompletion antes del parche
Después del parche, versión 10.0.22621.1105 de afd.sys.
Figura 4: afd!AfdNotifyRemoveIoCompletion posterior al parche
Este cambio que se muestra arriba es la única actualización de la función identificada. Un análisis rápido mostró que se está realizando una verificación basada en Si, por el contrario, no es cero, se llama a ProbeForWrite para garantizar que el puntero establecido en el campo es una dirección válida que reside en el modo de usuario.
Esta comprobación falta en la versión anterior al parche del controlador. Dado que la función tiene una declaración de cambio específica para
A partir de esta actualización, podemos inferir que un atacante puede llegar a esta ruta de código con un valor controlado enfield_0x18
El prototipo de función en sí contiene tanto el
Figura 5: prototipo de la función afd!AfdNotifyRemoveIoCompletion
Ahora sabemos la ubicación de la vulnerabilidad, pero no cómo desencadenar la ejecución de la ruta del código vulnerable. Haremos algo de ingeniería inversa antes de comenzar a trabajar en una prueba de concepto (PoC).
En primer lugar, se hizo una referencia cruzada de la función vulnerable para comprender dónde y cómo se utilizó.
Figura 6: Referencias cruzadas de afd!AfdNotifyRemoveIoCompletion
Se realiza una única llamada a la función vulnerable en
Repetimos el proceso, buscando referencias cruzadas a
Figura 7: afd!AfdIrpCallDispatch
Esta tabla contiene las rutinas de despacho para el controlador AFD. Las rutinas de envío se utilizan para gestionar las solicitudes de las aplicaciones Win32 mediante la llamada a DeviceIoControl. El código de control para cada función se encuentra en
Sin embargo, el puntero anterior no está dentro de la
Figura 8: afd!AfdIoctlTable
Vale la pena señalar que es el último código de control de entrada/salida (IOCTL) de la tabla, lo que indica que AfdNotifySock es probablemente una nueva función de envío que se agregó recientemente al controlador AFD.
En este punto, teníamos un par de opciones. Podríamos hacer ingeniería inversa de la API Winsock correspondiente en un espacio de usuario para entender mejor cómo se llama a la función del kernel subyacente, o hacer ingeniería inversa del código del kernel y llamarlo directamente. En realidad, no sabíamos a qué función de Winsock correspondía a
Nos encontramos con un código publicado por x86matthew que realiza operaciones de socket llamando directamente al controlador AFD, sin usar la biblioteca Winsock. Esto es interesante desde una perspectiva sigilosa, pero para nuestros propósitos, es una buena plantilla para crear un identificador para un socket TCP para realizar solicitudes IOCTL al controlador AFD. Desde ahí, pudimos llegar a la función objetivo, como lo demuestra el hecho de haber alcanzado un punto de interrupción establecido en WinDbg durante la depuración del kernel.
Figura 9: punto de interrupción de afd!AfdNotifySock
Ahora, consulte el prototipo de función para
En este punto, no sabemos cómo completar los datos en lpInBuffer, que llamaremos
Repasemos cada una de las verificaciones.
La primera comprobación que encontramos es al principio de
Figura 10: Comprobación del tamaño de afd!AfdNotifySock
Esta comprobación nos indica que el tamaño de la
La siguiente comprobación valida los valores de varios campos de nuestra estructura:
Figura 11: Validación de la estructura afd!AfdNotifySock
En ese momento no sabíamos a qué correspondía ninguno de los campos, por lo que pasamos en una
La siguiente comprobación que encontramos es luego de una llamada a ObReferenceObjectByHandle. Esta función toma el primer campo de nuestra estructura de entrada como su primer argumento.
Figura 12: afd!AfdNotifySock llama a nt!ObReferenceObjectByHandle
La llamada debe devolver un resultado satisfactorio para proceder a la ruta de ejecución de código correcta, lo que significa que debemos pasar un identificador válido a un
Luego, llegamos a un bucle cuyo contador era uno de los valores de nuestra estructura:
Figura 13: bucle afd!AfdNotifySock
Este bucle verificaba un campo de nuestra estructura para verificar que contenía un puntero válido en modo usuario y copiaba datos a él. El puntero se incrementa después de cada iteración del bucle. Completamos los punteros con direcciones válidas y establecimos el contador en 1. A partir de aquí, finalmente pudimos llegar a la función vulnerable
Figura 14: llamada a afd!AfdNotifyRemoveIoCompletion
Una vez dentro
Figura 15: afd! Comprobación del campo Afd!AfdNotifyRemoveIoCompletion
Por último, la última comprobación que hay que pasar antes de llegar al código de destino es una llamada a que debe devolver 0 ().
Esta función se bloqueará hasta que:
IoCompletionObject
parámetroControlamos el valor del tiempo de espera a través de nuestra estructura, pero simplemente establecer un tiempo de espera de 0 no es suficiente para que la función devuelva un resultado satisfactorio. Para que esta función se ejecute sin errores, debe haber al menos un registro de finalización disponible. Después de realizar una investigación, encontramos la función no documentada NtSetIoCompletion, que incrementa manualmente el contador de operaciones de E/S pendientes en un . Llamar a esta función en el que creamos anteriormente asegura que la llamada a devuelve .
Figura 16: afd!AfdNotifyRemoveIoCompletion check return nt!IoRemoveIoCompletion
Ahora que podemos llegar al código vulnerable, podemos completar el campo apropiado en nuestra estructura con una dirección arbitraria para escribir. El valor que escribimos en la dirección proviene de un número entero cuyo puntero se pasa a la llamada a
Figura 17: valor de retorno de nt!KeRemoveQueueEx
Figura 18: uso de retorno de nt!KeRemoveQueueEx
En nuestra prueba de concepto, este valor de escritura siempre es igual a . Especulamos que el valor de retorno de es el número de elementos eliminados de la cola, pero no investigó más a fondo. En este punto, teníamos la primitiva que necesitábamos y pasamos a terminar la cadena de exploit. Más tarde confirmamos que esta conjetura era correcta y que el valor de escritura puede incrementarse arbitrariamente mediante llamadas adicionales a en el .
Con la capacidad de escribir un valor fijo (0x1) en una dirección arbitraria del kernel, procedimos a convertir esto en un kernel arbitrario completo de lectura/escritura. Debido a que esta vulnerabilidad afecta a las últimas versiones de Windows 11(22H2), decidimos aprovechar la corrupción de objetos de entrada y salida de Windows de anillos para crear nuestra primitiva. Yarden Shafir ha escrito varios artículos excelentes sobre los anillos de E/S de Windows y también ha desarrollado y divulgado la primitiva que hemos aprovechado en nuestra cadena de explotación. Hasta donde sabemos, esta es la primera instancia en la que esta primitiva se ha utilizado en una explotación pública.
Cuando un usuario inicializa un anillo de E/S, se crean dos estructuras separadas, una en el espacio del usuario y otra en el espacio del kernel. Estas estructuras se muestran a continuación.
El objeto del núcleo se asigna a
Figura 19: inicialización de nt!_IORING_OBJECT
Tenga en cuenta que el objeto kernel tiene dos campos,
En el espacio de usuario, al llamar a kernelbase!CreateIoRing, se obtiene un identificador de anillo de E/S si la operación se realiza correctamente. Este identificador es un puntero a una estructura no documentada (HIORING). Nuestra definición de esta estructura se obtuvo de la investigación realizada por Yarden Shafir.
typedef struct _HIORING {
HANDLE handle;
NT_IORING_INFO Info;
ULONG IoRingKernelAcceptedVersion;
PVOID RegBufferArray;
ULONG BufferArraySize;
PVOID Unknown;
ULONG FileHandlesCount;
ULONG SubQueueHead;
ULONG SubQueueTail;
};
Si una vulnerabilidad, como la que se trata en esta entrada en el blog, le permite actualizar los
Como vimos anteriormente, podemos usar la vulnerabilidad para escribir 0x1 en cualquier dirección del kernel que nos guste. Para configurar la primitiva del anillo de entrada/salida podemos simplemente activar la vulnerabilidad dos veces.
En el primer activador configuramos el
Figura 20: nt!_IORING_OBJECT activa el error por primera vez
Y en el segundo activar asignamos RegBuffers a una dirección que podemos asignar en el espacio de usuario (como 0x0000000100000000).
Figura 21: nt!_IORING_OBJECT activa el error por segunda vez
Todo lo que queda es poner en cola las operaciones de E/S escribiendo punteros a
Figura 22. Configuración del espacio de usuario para la primitiva R/W del kernel del anillo de E/S
Uno de esos
Figura 23: ejemplo de operación de anillo de E/S falsificada
Para realizar una escritura arbitraria, una operación de E/S tiene la tarea de leer datos de un controlador de archivo y escribir esos datos en una dirección del kernel.
Figura 24: escritura arbitraria en el anillo de E/S
Por el contrario, para realizar una lectura arbitraria, se asigna una operación de E/S para leer datos en una dirección del núcleo y escribir esos datos en un identificador de archivo.
Figura 25: lectura arbitraria del anillo de E/S
Con la configuración primitiva todo lo que queda es usar algunas técnicas estándar posteriores a la explotación del kernel para filtrar el token de un proceso elevado como System (PID 4) y sobrescribir el token de un proceso diferente.
Después del lanzamiento público de nuestro código de exploit, Xiaoliang Liu (@flame36987044) de 360 Icesword Lab reveló públicamente por primera vez que descubrió una muestra que explota esta vulnerabilidad en la naturaleza (ITW) a principios de este año. La técnica utilizada por la muestra de ITW difería de la nuestra. El atacante activa la vulnerabilidad utilizando la función API Winsock correspondiente, , en lugar de llamar al controlador directamente, como en nuestro exploit.
La declaración oficial de 360 Icesword Lab es la siguiente:
“360 IceSword Lab se centra en la detección y defensa de APT. Con base en nuestro sistema de radar de vulnerabilidad de día 0, descubrimos una muestra de exploit de CVE-2023-21768 en enero de este año, que difiere de los exploits anunciados por @chompie1337 y @FuzzySec en que se explota a través de mecanismos del sistema y características de vulnerabilidad. El exploit está relacionado con y , obtiene el número de veces se llama, por lo que lo utilizamos para cambiar el recuento de privilegios”.
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.
Puede notar que en algunas partes de la ingeniería inversa nuestro análisis es superficial. A veces resulta útil limitarse a observar algunos cambios de estado relevantes y tratar partes del programa como una caja negra, para evitar perderse en detalles irrelevantes. Esto nos permitió revertir un exploit rápidamente, aunque maximizar la velocidad de finalización no era nuestro objetivo.
Además, realizamos una revisión de diferencias de parches de todas las vulnerabilidades reportadas enafd.sys
La falta de soporte para Supervisor Mode Access Protection (SMAP) en el kernel de Windows nos deja con abundantes opciones para construir nuevas primitivas de exploit de solo datos. Estas primitivas no son viables en otros sistemas operativos compatibles con SMAP. Por ejemplo, considere CVE-2021-41073, una vulnerabilidad en la implementación de Linux de búferes prerregistrados de anillos de E/S (la misma característica de la que abusamos en Windows para una primitiva R/W). Esta vulnerabilidad puede permitir sobrescribir un puntero del kernel para un búfer registrado, pero no se puede utilizar para construir una primitiva R/W arbitraria porque si el puntero se reemplaza por un puntero de usuario y el kernel intenta leer o escribir allí, el sistema se bloqueará.
A pesar de los esfuerzos de Microsoft por eliminar la posibilidad de explotar las primitivas, es inevitable que aparezcan nuevas primitivas que ocupen su lugar. Pudimos explotar la última versión de Windows 11 22H2 sin encontrar ninguna mitigación ni restricción por parte de las características de seguridad basadas en virtualización como HVCI.