¿Cómo funciona la vulnerabilidad JPEG of Death?

14 minutos de lectura

avatar de usuario
rafa

He estado leyendo acerca de un exploit anterior contra GDI+ en Windows XP y Servidor Windows 2003 llamado el JPEG de la muerte para un proyecto en el que estoy trabajando.

El exploit está bien explicado en el siguiente enlace:
http://www.infosecwriters.com/text_resources/pdf/JPEG.pdf

Básicamente, un archivo JPEG contiene una sección llamada COM que contiene un campo de comentario (posiblemente vacío) y un valor de dos bytes que contiene el tamaño de COM. Si no hay comentarios, el tamaño es 2. El lector (GDI+) lee el tamaño, resta dos y asigna un búfer del tamaño adecuado para copiar los comentarios en el montón. El ataque consiste en colocar un valor de 0 en el campo. GDI+ resta 2lo que lleva a un valor de -2 (0xFFFe) que se convierte en el entero sin signo 0XFFFFFFFE por memcpy.

Código de muestra:

unsigned int size;
size = len - 2;
char *comment = (char *)malloc(size + 1);
memcpy(comment, src, size);

Observa eso malloc(0) en la tercera línea debería devolver un puntero a la memoria no asignada en el montón. ¿Cómo puede escribir 0XFFFFFFFE bytes (4GB!!!!) posiblemente no cuelgue el programa? ¿Esto escribe más allá del área del montón y en el espacio de otros programas y el sistema operativo? ¿Qué pasa entonces?

Según entiendo memcpysimplemente copia n caracteres desde el destino hasta el origen. En este caso, el origen debe estar en la pila, el destino en el montón y n es 4GB.

  • malloc asignará memoria del montón. creo que el exploit se realizó antes de memcpy y después de que se asignó la memoria

    – iedoc

    6 de febrero de 2015 a las 16:01

  • solo como una nota al margen: es no memcpy lo que promueve el valor a un entero sin signo (4 bytes), sino la resta.

    – Rvdo

    6 de febrero de 2015 a las 18:24

  • Actualicé mi respuesta anterior con un ejemplo en vivo. los mallocEl tamaño de ed es solo de 2 bytes en lugar de 0xFFFFFFFE. Este enorme tamaño solo se usa para el tamaño de la copia, no para el tamaño de la asignación.

    – Neitsa

    15 de febrero de 2015 a las 15:04

avatar de usuario
Neitsa

Esta vulnerabilidad fue definitivamente una desbordamiento de montón.

¿Cómo es posible que escribir 0XFFFFFFFE bytes (4 GB !!!!) no bloquee el programa?

Probablemente lo hará, pero en algunas ocasiones tiene tiempo para explotar antes de que ocurra el bloqueo (a veces, puede hacer que el programa vuelva a su ejecución normal y evitar el bloqueo).

Cuando se inicia memcpy(), la copia sobrescribirá algunos otros bloques de almacenamiento dinámico o algunas partes de la estructura de administración del almacenamiento dinámico (p. ej., lista libre, lista ocupada, etc.).

En algún momento, la copia encontrará una página no asignada y activará una AV (infracción de acceso) al escribir. GDI+ luego intentará asignar un nuevo bloque en el montón (ver ntdll!RtlAllocateHeap) … pero las estructuras del montón ahora están desordenadas.

En ese momento, al diseñar cuidadosamente su imagen JPEG, puede sobrescribir las estructuras de administración del montón con datos controlados. Cuando el sistema intente asignar el nuevo bloque, probablemente desvinculará un bloque (libre) de la lista libre.

Los bloques se administran con (en particular) punteros de parpadeo (enlace hacia adelante; el siguiente bloque en la lista) y parpadeo (enlace hacia atrás; el bloque anterior en la lista). Si controla tanto el parpadeo como el parpadeo, es posible que tenga una posible ESCRITURA4 (escriba la condición Qué/Dónde) en la que controle lo que puede escribir y dónde puede escribir.

En ese punto, puede sobrescribir un puntero de función (SEH [Structured Exception Handlers] los punteros eran un objetivo de elección en ese momento en 2004) y obtener la ejecución del código.

Ver entrada de blog Montón de corrupción: un estudio de caso.

Nota: aunque escribí sobre la explotación usando la lista libre, un atacante podría elegir otra ruta usando otros metadatos del montón (“los metadatos del montón” son estructuras utilizadas por el sistema para administrar el montón; parpadear y parpadear son parte de los metadatos del montón), pero la explotación de desvinculación es probablemente la “más fácil”. Una búsqueda en Google de “explotación de montón” arrojará numerosos estudios sobre esto.

¿Esto escribe más allá del área del montón y en el espacio de otros programas y el sistema operativo?

Nunca. Los sistemas operativos modernos se basan en el concepto de espacio de direcciones virtuales, por lo que cada proceso tiene su propio espacio de direcciones virtuales que permite direccionar hasta 4 gigabytes de memoria en un sistema de 32 bits (en la práctica, solo tiene la mitad en el espacio del usuario, el resto es para el núcleo).

En resumen, un proceso no puede acceder a la memoria de otro proceso (excepto si se lo pide al kernel a través de algún servicio/API, pero el kernel verificará si la persona que llama tiene derecho a hacerlo).


Decidí probar esta vulnerabilidad este fin de semana, para que pudiéramos tener una buena idea de lo que estaba pasando en lugar de pura especulación. La vulnerabilidad ahora tiene 10 años, así que pensé que estaba bien escribir sobre ella, aunque no he explicado la parte de explotación en esta respuesta.

Planificación

La tarea más difícil fue encontrar un Windows XP con solo SP1, como lo fue en 2004 🙂

Luego, descargué una imagen JPEG compuesta solo por un solo píxel, como se muestra a continuación (corte por brevedad):

File 1x1_pixel.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF E0|00 10 4A 46|49 46 00 01|01 01 00 60| ÿØÿà JFIF  `
00000010  00 60 00 00|FF E1 00 16|45 78 69 66|00 00 49 49|  `  ÿá Exif  II
00000020  2A 00 08 00|00 00 00 00|00 00 00 00|FF DB 00 43| *          ÿÛ C
[...]

Una imagen JPEG se compone de marcadores binarios (que introducen segmentos). En la imagen de arriba, FF D8 es el marcador SOI (Start Of Image), mientras que FF E0por ejemplo, es un marcador de aplicación.

El primer parámetro en un segmento de marcador (excepto algunos marcadores como SOI) es un parámetro de longitud de dos bytes que codifica el número de bytes en el segmento de marcador, incluido el parámetro de longitud y excluyendo el marcador de dos bytes.

Simplemente agregué un marcador COM (0xFFFE) justo después del SOI, ya que los marcadores no tienen un orden estricto.

File 1x1_pixel_comment_mod1.JPG
Address   Hex dump                                         ASCII
00000000  FF D8 FF FE|00 00 30 30|30 30 30 30|30 31 30 30| ÿØÿþ  0000000100
00000010  30 32 30 30|30 33 30 30|30 34 30 30|30 35 30 30| 0200030004000500
00000020  30 36 30 30|30 37 30 30|30 38 30 30|30 39 30 30| 0600070008000900
00000030  30 61 30 30|30 62 30 30|30 63 30 30|30 64 30 30| 0a000b000c000d00
[...]

La longitud del segmento COM se establece en 00 00 para desencadenar la vulnerabilidad. También inyecté 0xFFFC bytes justo después del marcador COM con un patrón recurrente, un número de 4 bytes en hexadecimal, que resultará útil al “explotar” la vulnerabilidad.

depuración

Hacer doble clic en la imagen activará inmediatamente el error en el shell de Windows (también conocido como “explorer.exe”), en algún lugar de gdiplus.dllen una función llamada GpJpegDecoder::read_jpeg_marker().

Esta función se llama para cada marcador en la imagen, simplemente: lee el tamaño del segmento del marcador, asigna un búfer cuya longitud es el tamaño del segmento y copia el contenido del segmento en este búfer recién asignado.

Aquí el comienzo de la función:

.text:70E199D5  mov     ebx, [ebp+arg_0] ; ebx = *this (GpJpegDecoder instance)
.text:70E199D8  push    esi
.text:70E199D9  mov     esi, [ebx+18h]
.text:70E199DC  mov     eax, [esi]      ; eax = pointer to segment size
.text:70E199DE  push    edi
.text:70E199DF  mov     edi, [esi+4]    ; edi = bytes left to process in the image

eax registrar puntos al tamaño del segmento y edi es el número de bytes que quedan en la imagen.

Luego, el código procede a leer el tamaño del segmento, comenzando por el byte más significativo (la longitud es un valor de 16 bits):

.text:70E199F7  xor     ecx, ecx        ; segment_size = 0
.text:70E199F9  mov     ch, [eax]       ; get most significant byte from size --> CH == 00
.text:70E199FB  dec     edi             ; bytes_to_process --
.text:70E199FC  inc     eax             ; pointer++
.text:70E199FD  test    edi, edi
.text:70E199FF  mov     [ebp+arg_0], ecx ; save segment_size

Y el byte menos significativo:

.text:70E19A15  movzx   cx, byte ptr [eax] ; get least significant byte from size --> CX == 0
.text:70E19A19  add     [ebp+arg_0], ecx   ; save segment_size
.text:70E19A1C  mov     ecx, [ebp+lpMem]
.text:70E19A1F  inc     eax             ; pointer ++
.text:70E19A20  mov     [esi], eax
.text:70E19A22  mov     eax, [ebp+arg_0] ; eax = segment_size

Una vez hecho esto, el tamaño del segmento se utiliza para asignar un búfer, siguiendo este cálculo:

tamaño_asignación = tamaño_segmento + 2

Esto se hace con el siguiente código:

.text:70E19A29  movzx   esi, word ptr [ebp+arg_0] ; esi = segment size (cast from 16-bit to 32-bit)
.text:70E19A2D  add     eax, 2 
.text:70E19A30  mov     [ecx], ax 
.text:70E19A33  lea     eax, [esi+2] ; alloc_size = segment_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

En nuestro caso, como el tamaño del segmento es 0, el tamaño asignado para el búfer es de 2 bytes.

La vulnerabilidad está justo después de la asignación:

.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)
.text:70E19A3C  test    eax, eax
.text:70E19A3E  mov     [ebp+lpMem], eax ; save pointer to allocation
.text:70E19A41  jz      loc_70E19AF1
.text:70E19A47  mov     cx, [ebp+arg_4]   ; low marker byte (0xFE)
.text:70E19A4B  mov     [eax], cx         ; save in alloc (offset 0)
;[...]
.text:70E19A52  lea     edx, [esi-2]      ; edx = segment_size - 2 = 0 - 2 = 0xFFFFFFFE!!!
;[...]
.text:70E19A61  mov     [ebp+arg_0], edx

El código simplemente resta el tamaño del segmento_tamaño (la longitud del segmento es un valor de 2 bytes) del tamaño del segmento completo (0 en nuestro caso) y termina con un subdesbordamiento entero: 0 – 2 = 0xFFFFFFFE

Luego, el código verifica si quedan bytes para analizar en la imagen (lo cual es cierto), y luego salta a la copia:

.text:70E19A69  mov     ecx, [eax+4]  ; ecx = bytes left to parse (0x133)
.text:70E19A6C  cmp     ecx, edx      ; edx = 0xFFFFFFFE
.text:70E19A6E  jg      short loc_70E19AB4 ; take jump to copy
;[...]
.text:70E19AB4  mov     eax, [ebx+18h]
.text:70E19AB7  mov     esi, [eax]      ; esi = source = points to segment content ("0000000100020003...")
.text:70E19AB9  mov     edi, dword ptr [ebp+arg_4] ; edi = destination buffer
.text:70E19ABC  mov     ecx, edx        ; ecx = copy size = segment content size = 0xFFFFFFFE
.text:70E19ABE  mov     eax, ecx
.text:70E19AC0  shr     ecx, 2          ; size / 4
.text:70E19AC3  rep movsd               ; copy segment content by 32-bit chunks

El fragmento anterior muestra que el tamaño de la copia es 0xFFFFFFFE fragmentos de 32 bits. El búfer de origen está controlado (contenido de la imagen) y el destino es un búfer en el montón.

Condición de escritura

La copia desencadenará una excepción de infracción de acceso (AV) cuando llegue al final de la página de memoria (esto podría ser desde el puntero de origen o el puntero de destino). Cuando se activa el AV, el montón ya se encuentra en un estado vulnerable porque la copia ya ha sobrescrito todos los bloques de montón siguientes hasta que se encontró una página no asignada.

Lo que hace que este error sea explotable es que 3 SEH (controlador de excepciones estructurado; esto es prueba/excepto en un nivel bajo) detecta excepciones en esta parte del código. Más precisamente, el primer SEH desenrollará la pila para que vuelva a analizar otro marcador JPEG, omitiendo así por completo el marcador que activó la excepción.

Sin un SEH, el código habría bloqueado todo el programa. Entonces el código salta el segmento COM y analiza otro segmento. Así que volvemos a GpJpegDecoder::read_jpeg_marker() con un nuevo segmento y cuando el código asigna un nuevo búfer:

.text:70E19A33  lea     eax, [esi+2] ; alloc_size = semgent_size + 2
.text:70E19A36  push    eax             ; dwBytes
.text:70E19A37  call    _GpMalloc@4     ; GpMalloc(x)

El sistema desvinculará un bloque de la lista libre. Sucede que las estructuras de metadatos fueron sobrescritas por el contenido de la imagen; entonces controlamos el desvinculo con metadatos controlados. El siguiente código se encuentra en algún lugar del sistema (ntdll) en el administrador del montón:

CPU Disasm
Address   Command                                  Comments
77F52CBF  MOV ECX,DWORD PTR DS:[EAX]               ; eax points to '0003' ; ecx = 0x33303030
77F52CC1  MOV DWORD PTR SS:[EBP-0B0],ECX           ; save ecx
77F52CC7  MOV EAX,DWORD PTR DS:[EAX+4]             ; [eax+4] points to '0004' ; eax = 0x34303030
77F52CCA  MOV DWORD PTR SS:[EBP-0B4],EAX
77F52CD0  MOV DWORD PTR DS:[EAX],ECX               ; write 0x33303030 to 0x34303030!!!

Ahora podemos escribir lo que queramos, donde queramos…

avatar de usuario
miguelcms

Como no conozco el código de GDI, lo que se muestra a continuación es solo una especulación.

Bueno, una cosa que me viene a la mente es un comportamiento que he notado en algunos sistemas operativos (no sé si Windows XP tenía esto) cuando se asignaba con new / mallocen realidad puede asignar más que su RAM, siempre que no escriba en esa memoria.

Este es en realidad un comportamiento del Kernel de Linux.

De www.kernel.org:

Las páginas en el espacio de direcciones lineales del proceso no residen necesariamente en la memoria. Por ejemplo, las asignaciones realizadas en nombre de un proceso no se satisfacen de inmediato, ya que el espacio solo se reserva dentro de vm_area_struct.

Para ingresar a la memoria residente, se debe activar una falla de página.

Básicamente, debe ensuciar la memoria antes de que realmente se asigne en el sistema:

  unsigned int size=-1;
  char* comment = new char[size];

A veces, en realidad no hará una asignación real en RAM (su programa aún no usará 4 GB). Sé que he visto este comportamiento en Linux, pero no puedo replicarlo ahora en mi instalación de Windows 7.

A partir de este comportamiento es posible el siguiente escenario.

Para que esa memoria exista en la RAM, debe ensuciarlo (básicamente memset o algún otro tipo de escritura):

  memset(comment, 0, size);

Sin embargo, la vulnerabilidad explota un desbordamiento de búfer, no una falla de asignación.

En otras palabras, si tuviera que tener esto:

 unsinged int size =- 1;
 char* p = new char[size]; // Will not crash here
 memcpy(p, some_buffer, size);

Esto conducirá a una escritura después del búfer, porque no existe un segmento de 4 GB de memoria continua.

No pusiste nada en p para ensuciar los 4 GB de memoria, y no sé si memcpy ensucia la memoria de una vez, o solo página por página (creo que es página por página).

Eventualmente terminará sobrescribiendo el marco de la pila (desbordamiento del búfer de la pila).

Otra vulnerabilidad más posible era que la imagen se mantuviera en la memoria como una matriz de bytes (leer el archivo completo en el búfer), y el tamaño de los comentarios se usara solo para saltar información no vital.

Por ejemplo

     unsigned int commentsSize = -1;
     char* wholePictureBytes; // Has size of file
     ...
     // Time to start processing the output color
     char* p = wholePictureButes;
     offset = (short) p[COM_OFFSET];
     char* dataP = p + offset;
     dataP[0] = EvilHackerValue; // Vulnerability here

Como mencionó, si el GDI no asignó ese tamaño, el programa nunca fallará.

  • Eso podría ser con un sistema de 64 bits, donde 4 GB no es un gran problema (hablando de espacio adicional). Pero en un sistema de 32 bits (también parecen ser vulnerables) no puede reservar 4 GB de espacio de direcciones, ¡porque eso sería todo lo que hay! entonces un malloc(-1U) seguramente fallara, volvera NULL y memcpy() se estrellará

    – rodrigo

    6 de febrero de 2015 a las 16:24

  • No creo que esta línea sea cierta: “Eventualmente terminará escribiendo en otra dirección de proceso”. Normalmente, un proceso no puede acceder a la memoria de otro. Ver Beneficios de la UMM.

    – chue x

    06/02/2015 a las 19:49


  • @MMU Benefits sí, tienes razón. Quería decir que superará los límites normales del montón y comenzará a sobrescribir el marco de la pila. Editaré mi respuesta, gracias por señalarlo.

    – MichaelCMS

    11 de febrero de 2015 a las 11:19

¿Ha sido útil esta solución?