Hasta ahora en los retos de la categoría 'Binary Exploitation' que he puesto en este blog se han visto involucrados ejecutables de 32 bits, ¿se explotaría igual la vulnerabilidad de desbordamiento de 'buffer' (en inglés, 'buffer overflow') con ejecutables de 64 bits?. Voy por partes. En este primer post explico cómo tomar el control del flujo de un ejecutable de 64 bits vulnerable y conseguir que el programa ejecute una determinada parte de él que nosotros queramos.
Para ello utilizo uno de los retos de la plataforma picoCTF 2019.
El desafío en cuestión, que lleva el título "NewOverFlow-1", presenta en mi opinión un nivel de dificultad alto (★★★★☆). Para saber por qué le asigno ese nivel de dificultad hay que esperar al final de esta entrada :).
- NewOverFlow-1 - Points: 200:
Su enunciado dice lo siguiente: 'Lets try moving to 64-bit, but don't worry we'll start easy. Overflow the buffer and change the return address to the flag function in this program. You can find it in /problems/newoverflow-1_3_e53f871ba121b62d35646880e2577f89 on the shell server. Source'.
Se proporcionan dos archivos: un ejecutable (vuln) y un fichero con el código fuente (vuln.c).
Y como pista ('Hint') se nos da la siguiente:
- 'Now that we're in 64-bit, what used to be 4 bytes, now may be 8 bytes'.
Solución: lo primero que hago es comprobar el tipo de fichero que es vuln:
Como se observa en la figura anterior se trata de un ejecutable de 64 bits.
Después lo ejecuto en local; se me pide que introduzca una cadena que me dé la 'flag', incluyo 'A' y el programa finaliza.
Echo ahora un vistazo al código fuente (vuln.c):
#include < stdio.h>
#include < stdlib.h>
#include < string.h>
#include < unistd.h>
#include < sys/types.h>
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("'flag.txt' missing in the current directory!\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
vuln();
return 0;
}
Como se observa, en este caso el tamaño del 'buffer' asignado para la cadena a introducir es de 64 bytes. Además, veo que para que el programa muestre la 'flag' debo tomar el control del flujo para que se ejecute la función flag. Para ello, la idea es la misma que en el caso del post que escribí sobre el reto titulado "buffer overflow 1", es decir, mediante un desbordamiento de 'buffer' sobrescribir la dirección de retorno de la función vuln para que el programa salte al inicio de la función flag.
Lo primeros que hago es obtener la dirección de inicio de la función flag. Para ello utilizo los comandos objdump y grep, de la siguiente manera:
Para ver la 'flag', la dirección a la que debo forzar que bifurque el programa cuando finalice la ejecución de la función vuln es 0x0000000000400767.
Ahora calculo la diferencia o desplazamiento que existe entre la posición de inicio del 'buffer', que tiene 64 bytes de tamaño asignado, y la de inicio de la dirección de retorno de la función vuln, ya que ese será el tamaño en bytes del "relleno" de la cadena a introducir antes de incluir como parte final de dicha cadena la dirección de inicio de la función flag.
Para calcular el "relleno" indicado en el párrafo anterior utilizo como 'debugger' el software gdb. Desensamblo la función principal (main):
Y veo que la dirección de retorno de la función vuln es 0x000000000040084a.
Desensamblo la función vuln, pongo un punto de ruptura ('breakpoint') en la última instrucción (vuln+27), ejecuto el programa, introduzco 'A' como cadena e inspecciono la información del 'frame' en la pila.
Como se ve la dirección de la siguiente instrucción a ejecutar es 0x4007e7 y la dirección de retorno de la función vuln es la que he indicado antes, 0x40084a, es decir, la instrucción inmediatamente siguiente a la llamada a la función vuln en la función principal main.
Ahora, finalizo la ejecución del programa, ejecuto otra vez el programa, introduzco una cadena que me pueda indicar que parte de la misma sobrescribe la dirección de retorno de la función vuln, por ejemplo: 64 caracteres 'A' (tamaño del 'buffer') + '111111112222222233333333...' y cuando éste se detiene inspecciono la información del 'frame' en la pila:
Como se observa, la dirección de retorno de la función vuln se ha sobrescrito con ocho bytes: 0x3232323232323232 (valores hexadecimales que se corresponden en ASCII con '22222222''), con lo que el desplazamiento es de 64 + 8 bytes y, por tanto, el "relleno" de la cadena a introducir antes de incluir como parte final de dicha cadena la dirección de inicio de la función flag es de 72 bytes.
Y, finalmente, mediante una pequeña línea codificada en python (hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian', es decir, del byte menos significativo al más significativo): python -c "print 'A' * 72 + '\x67\x07\x40\x00\x00\x00\x00\x00'" | ./vuln, obtengo lo siguiente:
Con lo que parece que este 'exploit' funciona; el programa ha bifurcado a la función flag y el mensaje que se muestra se debe a que estoy ejecutando el programa en local. Utilizo la misma línea codificada en python en el servidor:
Sin embargo, no se muestra la 'flag', ¿por qué?. Pues no lo tengo nada claro y después de investigar por Internet aún menos :(.
No obstante, y aunque como digo no me queda nada claro, parece ser que el problema se produce porque si los operandos no están alineados correctamente en la pila se genera una excepción, y en nuestro caso creo entender que para solucionar este problema bastaría con incluir 8 bytes más en la pila después del "relleno" con la dirección de inicio de otra función.
Voy a ver si consigo solucionarlo. Para ello, después de los 72 caracteres 'A' de relleno incluyo 8 bytes adicionales con la dirección de inicio de la función vuln 0x00000000004007cc (hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian') y después introduzco los 8 bytes con la dirección de inicio de la función flag 0x0000000000400767 (de igual forma hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian'). Es decir:
python -c "print 'A' * 72 + '\xcc\x07\x40\x00\x00\x00\x00\x00\x67\x07\x40\x00\x00\x00\x00\x00'" | ./vuln
Con lo que la 'flag' es: picoCTF{th4t_w4snt_t00_d1ff3r3nt_r1ghT?_bfd48203}
Como se ve ha funcionado, pero no lo comprendo del todo :(, por lo que si algún amable lector de este blog lo tiene claro agradecería la explicación del por qué.
Para ello utilizo uno de los retos de la plataforma picoCTF 2019.
El desafío en cuestión, que lleva el título "NewOverFlow-1", presenta en mi opinión un nivel de dificultad alto (★★★★☆). Para saber por qué le asigno ese nivel de dificultad hay que esperar al final de esta entrada :).
- NewOverFlow-1 - Points: 200:
Su enunciado dice lo siguiente: 'Lets try moving to 64-bit, but don't worry we'll start easy. Overflow the buffer and change the return address to the flag function in this program. You can find it in /problems/newoverflow-1_3_e53f871ba121b62d35646880e2577f89 on the shell server. Source'.
Se proporcionan dos archivos: un ejecutable (vuln) y un fichero con el código fuente (vuln.c).
Y como pista ('Hint') se nos da la siguiente:
- 'Now that we're in 64-bit, what used to be 4 bytes, now may be 8 bytes'.
Solución: lo primero que hago es comprobar el tipo de fichero que es vuln:
Como se observa en la figura anterior se trata de un ejecutable de 64 bits.
Después lo ejecuto en local; se me pide que introduzca una cadena que me dé la 'flag', incluyo 'A' y el programa finaliza.
Echo ahora un vistazo al código fuente (vuln.c):
#include < stdio.h>
#include < stdlib.h>
#include < string.h>
#include < unistd.h>
#include < sys/types.h>
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("'flag.txt' missing in the current directory!\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Welcome to 64-bit. Give me a string that gets you the flag: ");
vuln();
return 0;
}
Como se observa, en este caso el tamaño del 'buffer' asignado para la cadena a introducir es de 64 bytes. Además, veo que para que el programa muestre la 'flag' debo tomar el control del flujo para que se ejecute la función flag. Para ello, la idea es la misma que en el caso del post que escribí sobre el reto titulado "buffer overflow 1", es decir, mediante un desbordamiento de 'buffer' sobrescribir la dirección de retorno de la función vuln para que el programa salte al inicio de la función flag.
Lo primeros que hago es obtener la dirección de inicio de la función flag. Para ello utilizo los comandos objdump y grep, de la siguiente manera:
Para ver la 'flag', la dirección a la que debo forzar que bifurque el programa cuando finalice la ejecución de la función vuln es 0x0000000000400767.
Ahora calculo la diferencia o desplazamiento que existe entre la posición de inicio del 'buffer', que tiene 64 bytes de tamaño asignado, y la de inicio de la dirección de retorno de la función vuln, ya que ese será el tamaño en bytes del "relleno" de la cadena a introducir antes de incluir como parte final de dicha cadena la dirección de inicio de la función flag.
Para calcular el "relleno" indicado en el párrafo anterior utilizo como 'debugger' el software gdb. Desensamblo la función principal (main):
Desensamblo la función vuln, pongo un punto de ruptura ('breakpoint') en la última instrucción (vuln+27), ejecuto el programa, introduzco 'A' como cadena e inspecciono la información del 'frame' en la pila.
Como se ve la dirección de la siguiente instrucción a ejecutar es 0x4007e7 y la dirección de retorno de la función vuln es la que he indicado antes, 0x40084a, es decir, la instrucción inmediatamente siguiente a la llamada a la función vuln en la función principal main.
Ahora, finalizo la ejecución del programa, ejecuto otra vez el programa, introduzco una cadena que me pueda indicar que parte de la misma sobrescribe la dirección de retorno de la función vuln, por ejemplo: 64 caracteres 'A' (tamaño del 'buffer') + '111111112222222233333333...' y cuando éste se detiene inspecciono la información del 'frame' en la pila:
Como se observa, la dirección de retorno de la función vuln se ha sobrescrito con ocho bytes: 0x3232323232323232 (valores hexadecimales que se corresponden en ASCII con '22222222''), con lo que el desplazamiento es de 64 + 8 bytes y, por tanto, el "relleno" de la cadena a introducir antes de incluir como parte final de dicha cadena la dirección de inicio de la función flag es de 72 bytes.
Y, finalmente, mediante una pequeña línea codificada en python (hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian', es decir, del byte menos significativo al más significativo): python -c "print 'A' * 72 + '\x67\x07\x40\x00\x00\x00\x00\x00'" | ./vuln, obtengo lo siguiente:
Con lo que parece que este 'exploit' funciona; el programa ha bifurcado a la función flag y el mensaje que se muestra se debe a que estoy ejecutando el programa en local. Utilizo la misma línea codificada en python en el servidor:
No obstante, y aunque como digo no me queda nada claro, parece ser que el problema se produce porque si los operandos no están alineados correctamente en la pila se genera una excepción, y en nuestro caso creo entender que para solucionar este problema bastaría con incluir 8 bytes más en la pila después del "relleno" con la dirección de inicio de otra función.
Voy a ver si consigo solucionarlo. Para ello, después de los 72 caracteres 'A' de relleno incluyo 8 bytes adicionales con la dirección de inicio de la función vuln 0x00000000004007cc (hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian') y después introduzco los 8 bytes con la dirección de inicio de la función flag 0x0000000000400767 (de igual forma hay que tener en cuenta que el formato de almacenamiento en memoria es 'little-endian'). Es decir:
python -c "print 'A' * 72 + '\xcc\x07\x40\x00\x00\x00\x00\x00\x67\x07\x40\x00\x00\x00\x00\x00'" | ./vuln
Con lo que la 'flag' es: picoCTF{th4t_w4snt_t00_d1ff3r3nt_r1ghT?_bfd48203}
Como se ve ha funcionado, pero no lo comprendo del todo :(, por lo que si algún amable lector de este blog lo tiene claro agradecería la explicación del por qué.
Comentarios
Publicar un comentario