c++ - ¿Cómo funciona la vulnerabilidad JPEG of Death?
security memcpy (2)
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 apropiado para copiar los comentarios en el montón.
El ataque implica colocar un valor de
0
en el campo.
GDI + resta
2
, lo 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);
Observe que
malloc(0)
en la tercera línea debería devolver un puntero a la memoria no asignada en el montón.
¿Cómo es posible que escribir
0XFFFFFFFE
bytes (
4GB
!!!!) no bloquee el programa?
¿Esto escribe más allá del área de almacenamiento dinámico y en el espacio de otros programas y el sistema operativo?
¿Qué pasa entonces?
Según entiendo
memcpy
, simplemente copia
n
caracteres del destino a la fuente.
En este caso, el origen debe estar en la pila, el destino en el montón
n
es de
4GB
.
Como no conozco el código de GDI, lo que sigue es solo 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 new /
malloc
, en realidad puede asignar más que su RAM, siempre que ya que no escribes en ese recuerdo.
Esto es realmente 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 un fallo 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 ahora no puedo replicarlo 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):
memset(comment, 0, size);
Sin embargo, la vulnerabilidad explota un desbordamiento del búfer, no una falla 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 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 pila).
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 mencionó, si el GDI no asignó ese tamaño, el programa nunca se bloqueará.
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 tienes tiempo para explotar antes de que ocurra el bloqueo (a veces, puedes 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 de almacenamiento dinámico (por ejemplo, lista libre, lista ocupada, etc.).
En algún momento, la copia encontrará una página no asignada y desencadenará una AV (Infracción de acceso) al escribir. Luego, GDI + intentará asignar un nuevo bloque en el montón (consulte ntdll!RtlAllocateHeap ) ... pero las estructuras del montón ahora están en mal estado.
En ese punto, al crear cuidadosamente su imagen JPEG, puede sobrescribir las estructuras de administración de almacenamiento dinámico con datos controlados. Cuando el sistema intenta asignar el nuevo bloque, probablemente desvincule un bloque (libre) de la lista libre.
Los bloques se gestionan con (en particular) un puntero parpadeante (enlace hacia adelante; el siguiente bloque de la lista) y un parpadeo (enlace hacia atrás; el bloque anterior de la lista). Si controla tanto el parpadeo como el parpadeo, es posible que tenga un posible ESCRIBIR4 (escriba qué / dónde) donde controla lo que puede escribir y dónde puede escribir.
En ese momento, puede sobrescribir un puntero de función (los punteros SEH [Controladores de excepciones estructurados] eran un objetivo de elección en ese momento en 2004) y obtener la ejecución del código.
Vea la publicación del blog Heap Corruption: A Case Study .
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 utilizadas por el sistema para administrar el montón; parpadeo y parpadeo 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 del montón" arrojará numerosos estudios sobre esto.
¿Esto escribe más allá del área de almacenamiento dinámico 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 el direccionamiento de hasta 4 gigabytes de memoria en un sistema de 32 bits (en la práctica, solo obtiene la mitad en tierra de 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 solicita al núcleo a través de algún servicio / API, pero el núcleo verificará si la persona que llama tiene derecho a hacerlo).
Decidí probar esta vulnerabilidad este fin de semana, para poder tener una buena idea de lo que estaba sucediendo 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 era en 2004 :)
Luego, descargué una imagen JPEG compuesta solo de un píxel, como se muestra a continuación (cortada 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 está compuesta de marcadores binarios (que introducen segmentos).
En la imagen de arriba,
FF D8
es el marcador SOI (Inicio de imagen), 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 se establece en
00 00
para 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
Al hacer doble clic en la imagen, se activará de inmediato el error en el shell de Windows (también conocido como "explorer.exe"), en algún lugar de
gdiplus.dll
, en 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 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
registro
eax
apunta al tamaño del segmento y
edi
es el número de bytes que quedan en la imagen.
El código luego 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 = segmento_size + 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 de 2 bytes .
La vulnerabilidad es 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 flujo inferior entero: 0 - 2 = 0xFFFFFFFE
El código luego verifica si hay bytes restantes 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 trozos 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 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 encontró una página no asignada.
Lo que hace que este error sea explotable es que 3 SEH (Structured Exception Handler; esto es try / except a nivel bajo) están capturando 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 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 omite el segmento COM y analiza otro segmento.
Entonces volvemos a
GpJpegDecoder::read_jpeg_marker()
con un nuevo segmento y cuando el código asigna un nuevo buffer:
.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 gratuita. Sucede que las estructuras de metadatos fueron sobrescritas por el contenido de la imagen; entonces controlamos la desvinculación con metadatos controlados. El siguiente código en algún lugar del sistema (ntdll) en el administrador de 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 ...