STACK-FOUR amd64
Última actualización
Última actualización
Este nivel explica lo que puede pasar si tenemos la capacidad de sobrescribir el puntero de instrucción rip. Esto es lo que se considera un Buffer Overflow estandar.
El puntero de instrucción no siempre se encuentra justo después de las variables en el STACK. Algunas cosas como el padding del propio compilador pueden afectar a su posición.
Algunas arquitecturas ni siquiera almacenan el puntero de instrucción en el stack.
Como en todos los anteriores, lo primero es utilizar rabin2
para obtener más información del binario:
Después lo podemos abrir en nuestro depurador, analizar todo y comprobar las funciones para ver qué estamos buscando:
Vemos que el binario utiliza las funciones printf, gets, puts y exit. Todas ellas las conocemos de los ejemplos anteriores así que ya sabemos que gets es explotable.
Además de eso, vemos que hay llamadas a tres funciones interesantes:
sym.complete_level
Como vemos, la función complete_level no tiene más que una llamada a puts() que no encierra ningún problema a estas alturas.
sym.start_level
Esta función ya tiene un funcionamiento más complejo. Cuando analicemos main, veremos start_level en profundidad.
Como vemos, en radare2 los offset se pueden nombrar de dos maneras:
qword [rbp - local_50h]
-> equivale a [rbp - 0x50]
qword [rbp - arg_8h]
-> equivale a [rbp + 0x8]
Esto se debe a que las variables locales se almacenan posteriormente al rbp y por tanto en direcciones de memoria más bajas mientras que los argumentos se almacenan previamente al rbp y por tanto en direcciones de memoria más altas.
No todo lo que se almacena en direcciones de memoria más altas son argumentos, como ahora veremos.
sym.main
La función main no encierra ninguna complejidad. Una llamada a puts para el BANNER y después llama directamente a la función start_level. Por este motivo, se debe analizar en profundidad la función start_level.
Si nos fijamos en la ejecución principal de la función, podemos entender un poco mejor lo que hace:
Como vemos, la función hace varias cosas:
Llamada a gets para llenar el buffer en [rbp - 0x50]
.
Justo después coge el valor de [rbp + 0x8]
y lo mete en [rbp - 0x8]
Por ultimo utiliza el valor de [rbp - 0x8]
en un printf junto con una string que dice algo referente a la dirección de retorno.
si analizamos el stack justo después de la llamada a gets, podremos averiguar que es lo que se almacena en [rbp - 0x8]
:
Vamos a preparar una prueba de concepto:
La introducimos en el debugger con rarun2:
Comprobamos el stack justo después de gets():
Al intentar utilizar "B" * 8, la dirección de retorno se veía afectada, por lo que para la explicación decidí utilizar "B" * 4 y dejar otros 4 espacios libres antes de la dirección de retorno.
Como podemos ver, al llenar el stack con datos, hemos alcanzado la dirección con offset [rbp + 0x4]
. Si analizamos la información de [rbp + 0x8]
podemos ver que tiene almacenada una dirección de memoria (Little Endian) que es 0x0040068d
Si vemos el desensamblado de la función main, vemos que esta dirección de memoria es la dirección siguiente a la de call start_level. Esto significa que hemos encontrado la dirección de retorno.
Ahora, lo único que nos queda es sobrescribir la dirección de retorno con una dirección que nos lleve a la función completelevel. En este caso, la primera dirección de complete_level es: 0x0040061d
.
A continuación podemos retocar nuestro exploit para que cumpla con nuestros requisitos:
Hemos conseguido sobrescribir la dirección de retorno y modificar el flujo del binario.
Después de comprobar que el exploit funciona podemos analizar el código fuente para comprobar qué hacía el binario exactamente.
Después de vencer el reto es importante tener en cuenta que cuando se hace referencia a [rbp + arg_8h]
en la función start_level, no se está haciendo referencia a un argumento sino a una información que se encuentra en direcciones mayores que el rbp. En este caso, lo que se referencia es la dirección de retorno.