¿Cómo funciona la vulnerabilidad JPEG of Death?

94

He estado leyendo sobre un exploit anterior contra GDI + en Windows XP y Windows Server 2003 llamado 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 implica poner un valor de 0en el campo. GDI + resta 2, lo que lleva a un valor del -2 (0xFFFe)cual se convierte al entero sin signo 0XFFFFFFFEpormemcpy .

Código de muestra:

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

Observe que malloc(0)en la tercera línea debe devolver un puntero a la memoria no asignada en el montón. ¿Cómo se puede escribir0XFFFFFFFE bytes ( 4GB!!!!) no bloquee 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 tengo entendido memcpy, simplemente copia los ncaracteres del destino al origen. En este caso, el origen debe estar en la pila, el destino en el montón y nes 4GB.

Rafa
fuente
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
solo como nota al margen: es no memcpy lo que promueve el valor de un entero sin signo (4 bytes), sino más bien la resta.
rev
1
Actualicé mi respuesta anterior con un ejemplo en vivo. El malloctamaño ed es de solo 2 bytes en lugar de0xFFFFFFFE . Este enorme tamaño solo se utiliza para el tamaño de la copia, no para el tamaño de asignación.
Neitsa

Respuestas:

96

Esta vulnerabilidad fue definitivamente un 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 tuvo 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 del montón o algunas partes de la estructura de gestión del montón (por ejemplo, lista libre, lista de ocupados, etc.).

En algún momento, la copia encontrará una página no asignada y activará un AV (infracción de acceso) al escribir. Luego, GDI + 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 crear cuidadosamente su imagen JPEG, puede sobrescribir las estructuras de administración de la pila con datos controlados. Cuando el sistema intente asignar el nuevo bloque, probablemente desvinculará un bloque (libre) de la lista libre.

Los bloques se gestionan con (notablemente) un flink (enlace de avance; el siguiente bloque de la lista) y un parpadeo (enlace de retroceso; el bloque anterior de la lista) punteros. Si controlas el parpadeo y el parpadeo, es posible que tengas una posible ESCRITURA4 (condición de escritura Qué / Dónde) donde controlas lo que puedes escribir y dónde puedes escribir.

En ese momento, puede sobrescribir un puntero de función ( SEH [Controladores de excepciones estructurados] eran un objetivo elegido en ese momento en 2004) y obtener la ejecución del código.

Ver la publicación de blog Corrupción en pilas: 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 ("metadatos del montón" son estructuras que utiliza el sistema para administrar el montón; flink y blink 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 la tierra del usuario, el resto es para el kernel).

En resumen, un proceso no puede acceder a la memoria de otro proceso (excepto si lo solicita 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 tiene ahora 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 D8es el marcador SOI (Start Of Image), mientras que FF E0, por 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 (0x FFFE) 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 está configurada 00 00para activar 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 será útil cuando "explote" 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 gdiplus.dll, en una función llamadaGpJpegDecoder::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 inicio 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 puntos de registro 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:

alloc_size = tamaño_segmento + 2

Esto se hace mediante 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 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 tamaño del segmento (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 activará 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 está en un estado vulnerable porque la copia ya ha sobrescrito todos los siguientes bloques de montón hasta que se encuentra una página no mapeada.

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

Sin un SEH, el código habría bloqueado todo el programa. Entonces, el código omite el segmento COM y analiza otro segmento. Entonces volvemos 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; por lo que controlamos la desvinculación con metadatos controlados. El siguiente código en algún lugar del sistema (ntdll) en el administrador de almacenamiento dinámico:

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

Neitsa
fuente
3

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) fue al asignar con nuevo / malloc , en realidad puede asignar más que su RAM, siempre y cuando no escribes en esa memoria.

Este es en realidad un comportamiento del kernel de Linux.

De www.kernel.org:

Las páginas del 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 inmediatamente ya que el espacio simplemente se reserva dentro de vm_area_struct.

Para entrar en la memoria residente, se debe activar un error de página.

Básicamente, necesita 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 ensuciarla (básicamente memset o alguna otra escritura en ella):

  memset(comment, 0, size);

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

En otras palabras, si tuviera esto:

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

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

No pusiste nada en p para ensuciar los 4 GB de memoria, y no sé si memcpyensucia 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 (Stack Buffer Overflow).

Otra vulnerabilidad más posible era si la imagen se guardaba en la memoria como una matriz de bytes (leer el archivo completo en el búfer) y el tamaño de los comentarios se usaba 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 mencionaste, si el GDI no asigna ese tamaño, el programa nunca fallará.

MichaelCMS
fuente
4
Eso podría ser con un sistema de 64 bits, donde 4GB no es un gran problema (hablando de espacio libre). Pero en un sistema de 32 bits, (parecen ser vulnerables también) no puede reservar 4GB de espacio de direcciones, ¡porque eso sería todo lo que hay! Así malloc(-1U)que seguramente fallará, regresará NULLy memcpy()se estrellará.
rodrigo
9
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. Consulte los beneficios de MMU .
chue x
Beneficios de @MMU sí, tienes razón. Se suponía que debí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ñalarla.
MichaelCMS