Los investigadores de ESET examinaron CVE-2025-50165, una grave vulnerabilidad de Windows descrita para permitir la ejecución remota de código con solo abrir un archivo JPG especialmente diseñado, uno de los formatos de imagen más utilizados. El fallo, encontrado y documentado por Zscaler ThreatLabz, despertó nuestro interés, ya que Microsoft evaluó su gravedad como crítica, pero consideró su explotabilidad como menos probable. Nuestro análisis de la causa raíz nos permitió determinar la ubicación exacta del código defectuoso y reproducir el fallo. Creemos que el escenario de explotación es más difícil de lo que parece.

Puntos clave de este blogpost:
  • Los investigadores de ESET ofrecen un análisis en profundidad de la vulnerabilidad CVE-2025-50165, ilustrado con fragmentos de pseudocódigo.
  • Proporcionamos nuestro método para reproducir el fallo utilizando una simple imagen JPG de 12 o 16 bits, y un examen del parche publicado inicialmente.
  • CVE-2025-50165 es un fallo en el proceso de codificación y compresión de una imagen JPG, no en su descodificación.
  • Nuestra conclusión explora y reevalúa la explotabilidad y el escenario de ataque de este fallo.

Resumen

El 20 de noviembre de 2025, Zscaler ThreatLabz publicó un artículo documentando el descubrimiento de CVE-2025-50165, una vulnerabilidad de alto impacto de ejecución remota de código presente en WindowsCodecs.dll. Esta biblioteca es la principal biblioteca de interfaz de Windows responsable del manejo de los formatos de archivo de imagen más comunes, como JPG, PNG, GIF, BMP, etc. Los hallazgos de Zscaler, así como la descripción proporcionada por Microsoft, revelan que el fallo tiene su origen en la desreferencia de un puntero de función no inicializado en WindowsCodecs.dll, que forma parte del componente de imágenes de Windows. El primero consiguió localizar el problema de desreferencia en la función jpeg_finish_compress, que se ejecuta cuando se comprime y (re)codifica un flujo de imágenes JPG. Cuando se trata de vulnerabilidades en el manejo de imágenes, lo primero en lo que se piensa es en errores de análisis y descodificación que se producen en cuanto se procesa la imagen. Así que esta ruta de código vulnerable bastante específica nos dejó con algunas preguntas que queríamos responder:

  • ¿Cuáles son las condiciones exactas para tomar la ruta de código vulnerable que lleva a la desreferencia del puntero de función no inicializado?
  • ¿Cuándo se llama a jpeg_finish_compress?
  • ¿Por qué no se inicializa el puntero de función?

Dado lo comunes que son las imágenes JPG en la web, queríamos respuestas, así que empezamos por investigar el código que causaba el fallo.

Crash site

Según la descripción de la entrada CVE-2025-50165, están afectadas las versiones de WindowsCodecs.dll a partir de la 10.0.26100.0 y anteriores a la 10.0.26100.4946. Analizamos la versión vulnerable 10.0.26100.4768 (SHA-1: 5887D96565749067564BABCD3DC5D107AB6666BD) y, a continuación, realizamos una comparación binaria con la primera versión parcheada 10.0.26100.4946 (SHA-1: 4EC1DC0431432BC318E78C520387911EC44F84FC). Tras descargar los símbolos correspondientes, examinamos la función que falla, jpeg_finish_compress. Según una cadena de versión presente en WindowsCodecs.dll - libjpeg-turbo versión 3.0.2 (build 20250529) - la DLL depende de una implementación bastante antigua de la biblioteca libjpeg-turbo (publicada el 24 de enero de 2024) para manejar imágenes JPG. El repositorio disponible públicamente nos ha permitido mapear la mayor parte del código binario y las estructuras relevantes a sus equivalentes en código fuente. Por ejemplo, la versión compilada de jpeg_finish_compress es muy similar a su equivalente en código fuente disponible aquí, como se muestra en la Figura 1.

Figure 1. Hex-Rays IDA decompiler output of the jpeg_finish_compress function
Figura 1. La salida del descompilador IDA de Hex-Rays de la función jpeg_finish_compress es similar al código fuente citado

Según las conclusiones de Zscaler, el fallo se produce en jpeg_finish_compress+0xCC, que corresponde a la línea 48 de la Figura 1, al desreferenciar un puntero de función situado en el offset 0x10 de una estructura desconocida(pub). Según el código fuente de libjpeg-turbo, esto corresponde a un puntero de función llamado compress_data_12. Para llegar a esta ruta específica, el miembro data_precision de jpeg_compress_struct debe establecerse en 12. Esto corresponde a la profundidad de bits o, en otras palabras, al número de bits utilizados para describir los colores. Básicamente, WindowsCodecs.dll se bloquea cuando intenta codificar una imagen JPG con una precisión de 12 bits.

Difusión de parches y análisis de la causa raíz

Utilizando Diaphora, una herramienta de diferenciación binaria, realizamos una diferenciación entre la versión vulnerable 10.0.26100.4768 y la versión parcheada 10.0.26100.4946, como se muestra en la Figura 2.

Figure 2. partially matched and unmatched functions between both libraries
Figura 2. Diaphora destacó con éxito las funciones parcialmente coincidentes y no coincidentes entre ambas bibliotecas

Sorprendentemente, la función de bloqueo jpeg_finish_compress mencionada en el artículo no está presente. Sin embargo, se modificaron dos funciones relacionadas con la codificación: rawtransencode_master_selection y jinit_c_rawtranscode_coef_controller_turbo. La diferencia entre las versiones vulnerable y parcheada de rawtransencode_master_selection se muestra en la Figura 3.

Figure 3. Diff between vulnerable and patched versions of rawtransencode_master_selection
Figura 3. Diferencia entre las versiones vulnerable (izquierda) y parcheada (derecha) de rawtransencode_master_selection

La única diferencia relevante parece ser que la función jinit_c_rawtranscode_coef_controller_turbo, que antes estaba inlineada en el cuerpo de la función rawtransencode_master_selection, ahora está separada. Si observamos la versión parcheada de la función jinit_c_rawtranscode_coef_controller_turbo, veremos que el miembro de la estructura compress_data_12, que antes no se inicializaba, ahora apunta a una función llamada rawtranscode_compress_output_16, como se muestra en la Figura 4.

Figure 4. Patched version of jinit_c_rawtranscode_coef_controller_turbo
Figura 4. Versión parcheada de jinit_c_rawtranscode_coef_controller_turbo

Nótese que el campo compress_data_16, que tampoco estaba inicializado en la versión vulnerable, también está configurado para apuntar a rawtranscode_compress_output_16 en la versión parcheada. Esta función es simplemente una función "stub" que llama a rawtranscode_compress_output, lo que puede indicar que no hay código específico para manejar imágenes JPG de 12 o 16 bits de precisión.

Reproducción del fallo

Como se menciona en el artículo de Zscaler, se puede compilar el fragmento de código propuesto por Microsoft(https://learn.microsoft.com/en-us/windows/win32/wic/-wic-codec-jpegmetadataencoding#jpeg-re-encode-example-code) para descodificar y recodificar una imagen JPG.

Una vez compilado este programa, se puede reproducir el fallo proporcionando un archivo JPG de 12 bits o de 16 bits. Revisando las muestras del repositorio libjpeg-turbo, se puede descargar una imagen de muestra de precisión de 12 bits en https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/testimages/testorig12.jpg. Al introducir esta imagen en la aplicación de ejemplo de recodificación, se produjo un fallo en el mismo punto que se menciona en el artículo de Zscaler. La figura 5 muestra el contexto del fallo durante una sesión de depuración.

Figure 5. The re-encoding example application crashes
Figura 5. La aplicación de ejemplo de recodificación se bloquea durante la rutina de compresión al manejar una imagen JPG de 12 bits

El valor hexadecimal repetido 0xBAADF00D apuntado por la dirección de memoria es un valor mágico utilizado por el heap del tiempo de ejecución de C (CRT) cuando un programa llama a HeapAlloc para asignar memoria. Marca la memoria como no inicializada ( véase https://www.nobugs.org/developer/win32/debug_crt_heap.html).

Como se ha indicado anteriormente, las dos versiones analizadas de WindowsCodecs.dll parecen ser capaces de manejar imágenes JPG de 16 bits de precisión. Pero cuando se prueban dichas imágenes, la aplicación de recodificación se bloquea al desreferenciar el puntero de función compress_data_16, como se observa en la Figura 6.

Figure 6. The re-encoding example application crashes during the compression routine
Figura 6. La aplicación de ejemplo de recodificación se bloquea durante la rutina de compresión al manejar una imagen JPG de 16 bits

Tras reproducir el fallo, nos preguntamos si esta vulnerabilidad específica también estaba presente en el código fuente de la biblioteca libjpeg-turbo.

Exploración del código fuente

Revisando los commits de libjpeg-turbo descubrimos que problemas similares se resolvieron el 18 de diciembre de 2024, con el commit e0e18de, que introdujo la versión 3.1.1. Esencialmente, el commit se asegura de que las estructuras son inicializadas a cero y que se produce un error si un puntero es NULL. Resulta que todas las inicializaciones a cero y comprobaciones introducidas por este commit están ausentes en las versiones vulnerables y parcheadas de WindowsCodecs.dll.

El mensaje del parche también insinúa otras posibles rutas de código vulnerables y, lo que es más importante, que también pueden producirse fallos en el proceso de descompresión al manipular una imagen JPG, como pone de manifiesto el diff del archivo jdapistd.c, ilustrado en la Figura 7.

Figure 7. Diff of decompression routines
Figura 7. Diferencia de las rutinas de descompresión implementadas en jdapistd.c

Como especifica claramente la descripción del commit, una aplicación que llame a jdapistd.c se bloqueará (debido a la desreferencia de un puntero de función no inicializado) sólo si cambia erróneamente el campo data_precision después de llamar a las rutinas jpeg_start_compress o jpeg_start_decompress. Esto crea un escenario bastante específico y probablemente poco realista en el que una aplicación que utilice WindowsCodecs.dll podría alterar el estado de las estructuras internas. Aunque tales aplicaciones pueden existir, no parece que la API de componentes de imágenes de Windows permita tal comportamiento.

Explotabilidad

Según revela nuestro análisis de la causa raíz, el problema principal de CVE-2025-50165 reside en el manejo por parte de WindowsCodecs.dll de imágenes JPG con un valor de precisión de datos distinto del convencional y estándar de 8 bits. Los dos punteros de función específicos de precisión(compress_data_12 y compress_data_16) no se inicializaban durante el proceso de compresión, creando dos rutas de código vulnerables que parecen ser accesibles sólo cuando se (re)codifica una imagen JPG. El simple hecho de abrir, y por tanto descodificar y renderizar, una imagen especialmente diseñada no desencadenará la vulnerabilidad. Sin embargo, la función vulnerable jpeg_finish_compress podría ser invocada si la imagen se guarda o si una aplicación host, como la aplicación Microsoft Photos, crea miniaturas de imágenes, como se muestra en la Figura 8.

Figure 8. The vulnerable jpeg_finish_compress function is called
Figura 8. La funciónvulnerable jpeg_finish_compress es invocada durante la creación de una miniatura de una imagen

Para que un programa se considere vulnerable, debe reunir las siguientes características

  • hace uso de una versión vulnerable de WindowsCodecs.dll,
  • no se bloquea ni aborta al descodificar un archivo JPG de 12 o 16 bits, y
  • permite recodificar la imagen.

Además, como mencionan los investigadores de Zscaler, para explotar esta vulnerabilidad es obligatorio que haya una fuga de direcciones y suficiente control sobre la pila.

Conclusión

Aunque JPG es más antiguo, ampliamente utilizado y quizás el formato de imagen digital más popular en las pruebas fuzz, todavía se pueden encontrar vulnerabilidades en algunos códecs. Este estudio de CVE-2025-50165 también destaca la importancia de mantenerse al día con las actualizaciones de seguridad cuando se utilizan bibliotecas de terceros.

El análisis de la causa raíz junto con la diferenciación de parches resultó ser una combinación muy potente que nos permitió responder a nuestras preguntas iniciales. Descubrimos que el fallo puede desencadenarse cuando WindowsCodecs.dll codifica un flujo JPG de precisión de 12 o 16 bits, ya que los punteros de función específicos de ambas precisiones no se inicializaban ni comprobaban antes de ser desreferenciados. Además, averiguamos que este proceso ocurre cuando se guarda una imagen de este tipo o cuando se crea una miniatura a partir de ella.

Esta investigación nos llevó a una conclusión similar a la de Microsoft en cuanto a la explotabilidad de esta vulnerabilidad. De hecho, como WindowsCodecs.dll es una biblioteca, una aplicación host se consideraría vulnerable si permite que las imágenes JPG sean (re)codificadas, y explotable sólo si un atacante tiene suficiente control sobre la aplicación (fuga de direcciones, manipulación de la pila). Teniendo todo esto en cuenta, parece que la explotación es poco probable.

Por último, vale la pena mencionar que, a partir de este escrito y de acuerdo con nuestras pruebas, las versiones más recientes de WindowsCodecs.dll (como 10.0.22621.6133, SHA-1: 3F3767D05E5A91184005D98427074711F68D9950) implementan los diferentes cambios mencionados en el commit de libjpeg-turbo, abordando eficazmente la falta de inicialización y verificación de punteros de función.