APT593

CPCTF: ROP n Morty (Rumania)

15/07/2020

Descripción

Salta Morty, saltaaaaaaa…

descargar aquí

Código fuente

Si bien durante el concurso no estuvo disponible el código fuente, el proceso de reversing es sencillo ya que se trataba de funciones básicas, y se deja como ejercicio al lector. Para fines didácticos, el código del reto es el siguiente:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char mibuffer[200] = "";
int checkpoint = 0;

void win() {
  strcpy(mibuffer, "echo -e '\\x4e\\x6f\\x20\\x6d\\x65\\x72\\x65\\x7a\\x63\\x6f\\x20\\x6c\\x61\\x20\\x66\\x6c\\x61\\x67\\x2e\\x2e\\x2e'");
  system(mibuffer);
  fflush(stdout);
}

void c1(char *name){
  checkpoint =1;
  strcpy(mibuffer, "cat ");
  if(strcmp(name, "3l_M4z_133t") != 0){
    strcpy(mibuffer, "cowsay Muuuuuuuu");
    checkpoint = 0;
    return;
  }
  printf("Que haces aqui??? Buscas mi flag? Te aseguro que no esta en f14g.txt\n");
}

void c2(char *loquesea){
  if(checkpoint == 1)
    strcat(mibuffer, loquesea);
}

void main() {
  char l33tn4m3[16];
  char nombre[64];

  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  printf("Cómo te llamas: ");
  gets(nombre);

  if(strcmp(l33tn4m3, "3l_Muy_133t") == 0){
    c1(l33tn4m3);
  }
  else{
    printf("Hola %s! Este servicio aun se encuentra en desarrollo... Vuelve pronto!\n", nombre);
    exit(0);
  }
}

Un chequeo rápido permite identificar un buffer overflow en la línea 39 de la función main(). Antes de plantear una estrategia, verifiquemos con que protecciones se compiló el ejecutable:

El binario tiene NX habilitado, lo que significa que aun si inyectaramos un shellcode en el stack por medio del input vulnerable, el stack estaría marcado como no ejecutable y no podríamos utilizarlo. Una alternativa viable consiste en utilizar ROP para armar nuestro shellcode en base a fragmentos del programa que terminen en un ret, pudiendo manipular el flujo de ejecución del programa por medio de un stack forjado que contenga varias direcciones de retorno que serán ejecutadas secuencialmente.

Nuestro objetivo final está en la función win() que contiene una llamada a la función system() que envía comandos a la consola directamente. El comando enviado depende de la variable global mibuffer, por lo que debemos llenar esa variable con un comando útil, ya que la función win() sólo lo llena de un echo por defecto. Para esto, deberemos forzar al programa a seguir el siguiente flujo de ejecución:

  1. Saltar del final de la función main() hacia la función c1(), donde la variable mibuffer es seteada a “cat ”
  2. Saltar a la función c2() donde se concatena a mibuffer el valor del parámetro loquesea
  3. Saltar finalmente a la línea con la función system() ubicada en medio de la función win()

Si bien esto suena a un ROP común, hay un par de obstaculos adicionales a vencer, pero nada complicado.

Logrando un crash en main()

Antes que nada, debemos lograr un segmentation fault en el programa para verificar que el buffer overflow puede explotarse. Si intentamos enviar una cantidad de texto grande, veremos que el programa no crashea independiente del tamaño del texto enviado.

Esto se debe a que el bloque IF en main() verifica que la variable l33tn4m3 tenga un valor de “3lMuy133t”, caso contrario termina el programa por medio de una llamada a exit() que impide el retorno de la función main. Al no acabar main(), el puntero de retorno que ha sido sobreescrito por nuestro input jamás es tomado en cuenta y por tanto no se da un segmentation fault, sino que se sale del programa de manera exitosa.

Nuestro primer objetivo será evitar la llamada a exit(), forzando al programa a tomar el otro branch del IF. Para esto debemos setear un valor específico en la variable l33tn4m3, pero esta no se utiliza durante el programa, haciendolo aparentemente imposible… Pero hay una opción: debemos recordar que las variables locales se almacenan una después de la otra en el stack. De hecho, nuestro stack al momento de ejecutarse la función main debiera verse algo así:

Stack en main

Si escribimos mas de 64 caracteres sobre la variable nombre, los que sigan desbordarán sobre la variable l33tn4m3 (y eventualmente el puntero de retorno de main), permitiéndonos modificarla. Como prueba rapida podemos generar el string requerido usando python y pasándolo al binario del reto:

Notar que es necesario poner un caracter NULL (\x00) al final de la cadena que sobreescribe l33tn4m3 como con cualquier cadena en C. Luego de eso rellenamos con letras B para sobreescribir el puntero de retorno y producir un segfault. El stack sobreescrito debiera verse algo asi:

Stack en main

Paso 1: entrando a c1()

Una vez hecho esto, podemos sobreescribir el puntero de retorno por la dirección de memoria de c1, que podemos obtener usando objdump:

Notar sin embargo, que una vez dentro de c1() requerimos que el parámetro name de la función esté seteado a “3lM4z133t”, ya que caso contrario mibuffer será sobreescrito por “cowsay Muuuuuuu” en lugar de “cat ”, además que la variable checkpoint será seteada a 0, lo que imposibilitaría utilizar c2(). Para analizar cómo se pasa el parámetro name a la función c1, abrimos Cutter: Stack en main

Como se puede ver, el primer argumento de la función c1() se pasa por medio del registro rdi. Si encontramos la manera de setear rdi a la dirección de memoria que apunta a la cadena “3lM4z133t”, lograremos saltarnos el bloque IF en c1(), saliendo de la función con mibuffer seteado a “cat “. Dado que estamos trabajando con ROP, podemos buscar un gadget en nuestro ejecutable que ejecute un “pop rdi, ret” permitiéndonos apuntar rdi a donde sea conveniente. Esto lo logramos facilmente usando ROPGadget:

Adicional a esto, averiguamos la dirección en memoria de la cadena “3lM4z133t” Stack en main

Con estos datos, armamos un primer exploit usando pwntools:

from pwn import *

C1  = 0x401299
POP_RDI = 0x40147b
EL_MAZ_LEET = 0x402008

p = process('./pwn2')
#gdb.attach(p)

p.recvuntil(':')
p64(C2) + p64(WIN) + b'\n')
p.send(b'A'*64 + b'3l_Muy_133t\x00' + b'A'*12 + p64(POP_RDI) + p64(EL_MAZ_LEET) + p64(C1) + b'\n')
p.interactive()

En resumen, nuestro exploit se encarga de cargar rdi con un puntero a la cadena “3lM4z133t”, que es interpretada como el parámetro name de la función c1(), evadiendo efectivamente el bloque IF que sobreescribe mibuffer, cargando “cat ” en mibuffer y seteando en 1 la variable checkpoint. Si lo ejecutamos, podemos ver que el printf() dentro de c1() se ejecuta, de modo que no se entró al IF:

Paso2: saltando a c2() y a win()

Al tener checkpoint en 1 y mibuffer inicializado con “cat ”, podemos usar c2() para concatenar al final del comando cualquier nombre de archivo de nuestra preferencia. La pista apunta claramente a “f14g.txt”. En lugar de intentar inyectar la cadena en memoria, usaremos directamente un puntero al final del mensaje arrojado por c1() donde se indica el nombre del archivo. Refiriéndonos una vez más a la sección de strings en Cutter, podemos ver que el mensaje se encuentra en: Stack en main

Por tanto el nombre del archivo puede hallarse 60 caracteres más adelante en 0x402054. Una vez más podemos analizar c2 para confirmar que el parámetro loquesea se pasa a la función a través de rdi, por lo que podemos volver a utilizar el gadget encontrado anteriormente para cargar rdi con la posición del nombre del archivo antes de saltar en c2().

Si esto se ejecuta de manera correcta, habremos logrado cargar mibuffer con “cat f14g.txt”, y solo nos resta saltar a win() para obtener el flag. Tener en cuenta, sin embargo, que no debemos saltar al inicio de win(), sino directamente a la ejecución de la función system(). Si saltaramos al comienzo, la variable mibuffer sería sobreescrita con otro comando. Abriendo la función win en cutter, podemos identificar el punto al que debemos saltar: Stack en main

Notar que system() requiere que se le pase una cadena como argumento a través de rdi, por lo que no saltamos directamente a la dirección de memoria de la llamada a system() sino una instrucción antes, donde se carga rdi con el puntero a mibuffer. Con esto, debieramos obtener el valor del flag.

Ajustando nuestro exploit tendríamos:

from pwn import *

EL_MAZ_LEET = 0x402008
POP_RDI = 0x40147b
WIN = 0x40127b
C1  = 0x401299
C2  = 0x40131a
FLAG_TXT = 0x00402054

p = process('./pwn2')
#gdb.attach(p)

p.recvuntil(':')
p.send(b'A'*64 + b'3l_Muy_133t\x00' + b'A'*12 + p64(POP_RDI) + p64(EL_MAZ_LEET) + p64(C1) + p64(POP_RDI) + p64(FLAG_TXT) + p64(C2) + p64(WIN) + b'\n')
p.interactive()

Ejecutando el exploit final tenemos: