Analizando CVE-2021-1665 – Vulnerabilidad de ejecución remota de código en Windows GDI +


Introducción

Microsoft Windows Graphics Device Interface +, también conocido como GDI +, permite que varias aplicaciones utilicen diferentes funciones gráficas en pantallas de video e impresoras. Las aplicaciones de Windows no acceden directamente al hardware de gráficos como los controladores de dispositivos, pero interactúan con GDI, que a su vez interactúa con los controladores de dispositivos. De esta manera, existe una capa de abstracción para las aplicaciones de Windows y un conjunto común de API para que todos las utilicen.

Debido a su formato complejo, GDI + tiene un historial conocido de varias vulnerabilidades. En McAfee, analizamos continuamente varios software de código abierto y de código cerrado, incluido Windows GDI +. En los últimos años, hemos informado a Microsoft sobre varios problemas en varios componentes de Windows, incluido GDI +, y hemos recibido CVE para ellos.

En esta publicación, detallamos nuestro análisis de la causa raíz de una de esas vulnerabilidades que encontramos usando WinAFL: CVE-2021-1665 – GDI + Remote Code Execution Vulnerability. Este problema se solucionó en enero de 2021 como parte de un parche de Microsoft.

¿Qué es WinAFL?

WinAFL es un puerto de Windows de un popular fuzzer AFL de Linux y es mantenido por Ivan Fratric de Google Project Zero. WinAFL usa instrumentación binaria dinámica usando DynamoRIO y requiere un programa llamado como arnés. Un arnés no es más que un programa simple que llama a las API que queremos eliminar.

Con WinAFL ya se proporcionó un arnés simple para esto, podemos habilitar "Imagen-> GetThumbnailImage”Código que fue comentado por defecto en el código. A continuación se muestra el código de aprovechamiento para fuzz GDI + image y GetThumbnailImage API:

Como puede ver, este pequeño fragmento de código simplemente crea un nuevo objeto de imagen a partir del archivo de entrada proporcionado y luego llama a otra función para generar una imagen en miniatura. Esto lo convierte en un excelente vector de ataque y puede afectar a varias aplicaciones de Windows si utilizan imágenes en miniatura. Además, esto requiere poca interacción del usuario, por lo que el software que usa GDI + y llama a la API GetThumbnailImage, es vulnerable.

Recolectando Corpus:

Un buen corpus proporciona una base sólida para el fuzzing. Para eso, podemos usar Google o GitHub, además de un corpus de prueba adicional disponible de varios software y archivos EMF públicos que se publicaron para otras vulnerabilidades. Hemos generado algunos archivos de prueba realizando cambios en un código de muestra proporcionado en el sitio de Microsoft que genera un archivo EMF con EMFPlusDrawString y otros registros:

Árbitro: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-emfplus/07bda2af-7a5d-4c0b-b996-30326a41fa57

Minimizando Corpus:

Una vez que hayamos recopilado un archivo corpus inicial, debemos minimizarlo. Para esto podemos usar una utilidad llamada winafl-cmin.py de la siguiente manera:

winafl-cmin.py -DD: \ trabajo \ winafl \ DynamoRIO \ bin32 -t 10000 -i inCorpus -o minCorpus -covtype edge -coverage_module gdiplus.dll -target_module gdiplus_hardik.exe -target_method fuzzMe -nargs_hardik .exe @@

¿Cómo funciona WinAFL?

WinAFL utiliza el concepto de fuzzing en memoria. Necesitamos proporcionar un nombre de función a WinAFL. Guardará el estado del programa al inicio de la función y tomará un archivo de entrada del corpus, lo mutará y lo enviará a la función.

Supervisará esto en busca de nuevas rutas de código o fallas. Si encuentra una nueva ruta de código, considerará el nuevo archivo como un caso de prueba interesante y lo agregará a la cola para una mayor mutación. Si encuentra fallas, guardará el archivo que falla en la carpeta de fallas.

La siguiente imagen muestra el flujo de fuzzing:

Fuzzing con WinAFL:

Una vez que hemos compilado nuestro programa de arnés, recopilado y minimizado el corpus, podemos ejecutar este comando para confundir nuestro programa con WinAFL:

afl-fuzz.exe -i minCorpus -o out -D D: work winafl DynamoRIO bin32 -t 20000 —coverage_module gdiplus.dll -fuzz_iterations 5000 -target_module gdiplus_hardik.exe -target_offset 0x16e0 -nargs 2 – gdip@lus_hardik.exe

Resultados:

Encontramos algunas fallas y, después de clasificar las fallas únicas, encontramos una falla en "gdiplus! BuiltLine :: GetBaselineOffset”Que se ve de la siguiente manera en la siguiente pila de llamadas:

Como se puede ver en la imagen de arriba, el programa falla al intentar leer datos de una dirección de memoria apuntada por edx + 8. Podemos ver que los registros ebx, ecx y edx contienen c0c0c0c0, lo que significa que el montón de páginas está habilitado para el binario. También podemos ver que c0c0c0c0 se pasa como parámetro a "gdiplus! FullTextImager :: RenderLine”Función.

Difusión de parches para ver si podemos encontrar la causa raíz

Para averiguar una causa raíz, podemos usar la diferencia de parches, es decir, podemos usar IDA BinDiff complemento para identificar qué cambios se han realizado en el archivo parcheado. Si tenemos suerte, podemos encontrar fácilmente la causa raíz con solo mirar el código que se cambió. Entonces, podemos generar un archivo IDB de versiones parcheadas y no parcheadas de gdiplus.dll y luego ejecutar IDA BinDiff plugin para ver los cambios.

Podemos ver que se agregó una nueva función en el archivo parcheado, y esto parece ser un destructor para BuiltLine Objeto:

También podemos ver que hay algunas funciones donde la puntuación de similitud es <1 y una de esas funciones es FullTextImager :: BuildAllLines Como se muestra abajo:

Ahora, solo para confirmar si esta función es realmente la que fue parcheada, podemos ejecutar nuestro programa de prueba y POC en windbg y establecer un punto de interrupción en esta función. Podemos ver que se alcanza el punto de interrupción y el programa ya no se bloquea:

Ahora, como siguiente paso, debemos identificar qué se ha cambiado en esta función para corregir esta vulnerabilidad. Para eso podemos comprobar el diagrama de flujo de esta función y vemos algo como sigue. Desafortunadamente, hay demasiados cambios para identificar la vulnerabilidad simplemente mirando la diferencia:

El lado izquierdo ilustra una dll sin parche, mientras que el lado derecho muestra un dll parcheado:

  • El verde indica que los bloques parcheados y no parcheados son los mismos.
  • Los bloques amarillos indican que ha habido algunos cambios entre las DLL sin parche y parcheadas.
  • Los bloques rojos indican diferencias en los dlls.

Si hacemos zoom en los bloques amarillos podemos ver lo siguiente:

Podemos notar varios cambios. Se eliminan pocos bloques en la DLL parcheada, por lo que la diferenciación de parches por sí sola no será suficiente para identificar la causa raíz de este problema. Sin embargo, esto presenta valiosos consejos sobre dónde buscar y qué buscar cuando se utilizan otros métodos de depuración como windbg. Algunas observaciones que podemos ver en la salida de bindiff anterior:

  • En la DLL sin parchear, si lo comprobamos con cuidado, podemos ver que hay una llamada a "GetuntrimmedCharacterCount"Función y luego hay otra llamada a una función"SetSpan :: SpanVector"
  • En la DLL parcheada, podemos ver que hay una llamada a "GetuntrimmedCharacterCount"Donde un valor de retorno almacenado dentro EAX se comprueba el registro. Si es cero, el control salta a otra ubicación: un incinerador de basuras por BuiltLine Objeto, este era un código recién agregado en la DLL parcheada:

Entonces podemos asumir que aquí es donde se corrige la vulnerabilidad. Ahora tenemos que averiguar lo siguiente:

  1. ¿Por qué nuestro programa se bloquea con el archivo POC proporcionado?
  2. ¿Qué campo del archivo está causando este bloqueo?
  3. ¿Qué valor tiene el campo?
  4. ¿Qué condición en el programa está causando este bloqueo?
  5. ¿Cómo se solucionó esto?

Formato de archivo EMF:

EMF también se conoce como formato de archivo meta mejorado que se utiliza para almacenar imágenes gráficas del dispositivo de forma independiente. Un archivo EMF consta de varios registros de longitud variable. Puede contener la definición de varios objetos gráficos, comandos para dibujar y otras propiedades gráficas.

Crédito: documentación de MS EMF.

Generalmente, un archivo EMF consta de los siguientes registros:

  1. Encabezado EMF – Contiene información sobre la estructura EMF.
  2. Registros EMF – Pueden ser varios registros de longitud variable, que contienen información sobre las propiedades de los gráficos, el orden de dibujo, etc.
  3. Registro EMF EOF – Este es el último registro en el archivo EMF.

Las especificaciones detalladas del formato de archivo EMF se pueden ver en el sitio de Microsoft en la siguiente URL:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-emf/91c257d7-c39d-4a36-9b1f-63e3f73d30ca

Ubicación del registro vulnerable en el archivo EMF:

Generalmente, la mayoría de los problemas en EMF se deben a registros corruptos o mal formados. Necesitamos averiguar qué tipo de registro está causando este bloqueo. Para esto, si miramos la pila de llamadas, podemos ver lo siguiente:

Podemos notar una llamada a funcionar "gdiplus! GdipPlayMetafileRecordCallback"

Al establecer un punto de interrupción en esta función y verificar el parámetro, podemos ver lo siguiente:

Podemos ver eso EDX contiene alguna dirección de memoria y podemos ver que los parámetros dados a esta función son: 00x00401c, 0x00000000 y 0x00000044.

Además, al comprobar la ubicación señalada por EDX podemos ver lo siguiente:

Si revisamos nuestro archivo POC EMF, podemos ver que estos datos pertenecen al archivo de offset: 0x15c:

Al pasar por la especificación EMF y analizar manualmente los registros, podemos descubrir fácilmente que se trata de una "EmfPlusDrawString ” registro, cuyo formato se muestra a continuación:

En nuestro caso:

Tipo de registro = 0x401c Registro EmfPlusDrawString

Banderas = 0x0000

Tamaño = 0x50

Tamaño de datos = 0x44

Brushid = 0x02

ID de formato = 0x01

Longitud = 0x14

Layoutrect = 00 00 00 00 00 00 00 00 FC FF C7 42 00 00 80 FF

Datos de cadena =

Ahora que hemos localizado el registro que parece estar causando el bloqueo, lo siguiente es averiguar por qué nuestro programa falla. Si depuramos y verificamos el código, podemos ver que el control llega a una función "gdiplus! FullTextImager :: BuildAllLines”. Cuando descompilamos este código, podemos ver algo como esto:

El siguiente diagrama muestra la jerarquía de llamadas a funciones:

La ejecución baja en resumen:

  1. Adentro "Builtline :: BuildAllLines ” función, hay un ciclo while dentro del cual el programa asigna 0x60 bytes de memoria. Luego llama al "Builtline :: BuiltLine ”
  2. La "Builtline :: BuiltLine" La función mueve datos a la memoria recién asignada y luego llama "BuiltLine :: GetUntrimmedCharacterCount”.
  3. El valor de retorno de "BuiltLine :: GetUntrimmedCharacterCount"Se agrega al contador de bucle, que es ECX. Este proceso se repetirá hasta que el contador de bucle (ECX) es <longitud de la cadena (EAX), que es 0x14 aquí.
  4. El bucle comienza en 0, por lo que debería terminar en 0x13 o debería terminar cuando el valor de retorno de "GetUntrimmedCharacterCount ” es 0.
  5. Pero en los vulnerables DLL, el programa no termina debido a la forma en que aumenta el contador de bucle. Aquí, "BuiltLine :: GetUntrimmedCharacterCount ” devuelve 0, que se agrega al contador de bucle (ECX) y no aumenta el valor ECX. Asigna 0x60 bytes de memoria y crea otra línea, corrompiendo los datos que luego hacen que el programa se bloquee. El bucle se ejecuta para 21 veces en lugar de 20.

En detalle:

1. Dentro "Builtline :: BuildAllLines ” la memoria se asignará para 0x60 o 96 bytes, y en el depurador se verá de la siguiente manera:

2. Luego llama "BuiltLine :: BuiltLine”Y mueve los datos a la memoria recién asignada:

3. Esto sucede en el lado de un bucle while y hay una llamada de función a "BuiltLine :: GetUntrimmedCharacterCount”.

4. Devuelve el valor de "BuiltLine :: GetUntrimmedCharacterCount”Se almacena en una ubicación 0x12ff2ec. Este valor será 1 como se puede ver a continuación:

5. Este valor se agrega a ECX:

6. Luego hay una verificación que determina si ecx <eax. Si es verdadero, continuará el bucle, de lo contrario, saltará a otra ubicación:

7. Ahora, en la versión vulnerable, el bucle no existe si el valor de retorno de "BuiltLine :: GetUntrimmedCharacterCount"Es 0, lo que significa que este 0 se agregará a ECX y lo que significa que ECX no aumentará. Entonces el ciclo se ejecutará 1 vez más con el "ECX”Valor de 0x13. Por lo tanto, esto conducirá a que el bucle se ejecute 21 veces en lugar de 20 veces. Esta es la causa raíz del problema aquí.

Además, después de un poco de depuración, podemos averiguar por qué EAX contiene 14. Se lee del archivo POC en el desplazamiento: 0x174:

Si recordamos, este es el EmfPlusDrawString grabar y 0x14 es la longitud que mencionamos antes.

Posteriormente, el programa llega a "FullTextImager :: Renderizar”Corrompe el valor de EAX porque lee la memoria no utilizada:

Esto se pasará como argumento a "FullTextImager :: RenderLine"Función:

Más tarde, el programa se bloqueará al intentar acceder a esta ubicación.

Nuestro programa fallaba mientras procesaba EmfPlusDrawString grabar dentro del archivo EMF mientras accede a una ubicación de memoria no válida y procesa un campo de datos de cadena. Básicamente, el programa no verificaba el valor de retorno de "gdiplus! BuiltLine :: GetUntrimmedCharacterCount”Y esto resultó en tomar una ruta de programa diferente que corrompió el registro y varios valores de memoria, lo que finalmente provocó el bloqueo.

¿Cómo se solucionó este problema?

Como hemos descubierto al observar el parche diff anterior, se agregó una verificación que determinó el valor de retorno de "gdiplus! BuiltLine :: GetUntrimmedCharacterCount”Función.

Si el valor devuelto es 0, entonces programe xor's EBX que contiene contador y salta a una ubicación que llama al destructor para el objeto Builtline:

Aquí está el destructor que evita el problema:

Conclusión:

GDI + es un componente de Windows de uso muy común y una vulnerabilidad como esta puede afectar a miles de millones de sistemas en todo el mundo. Recomendamos a nuestros usuarios que apliquen las actualizaciones adecuadas y mantengan actualizada su implementación de Windows.

En McAfee, estamos continuamente investigando varias bibliotecas de código abierto y de código cerrado y trabajamos con los proveedores para solucionar dichos problemas al informarles de manera responsable, dándoles el tiempo adecuado para solucionar el problema y publicar las actualizaciones según sea necesario.

Agradecemos a Microsoft por trabajar con nosotros para solucionar este problema y lanzar una actualización.





Enlace a la noticia original