Ir al contenido principal

Binary Exploitation (IV): Solución Reto picoCTF 2019 "NewOverFlow-1"

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 vulnpongo 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 flagel 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é.

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema de

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblema establecido en dicha resolución, sino que éste se adopta también como im