lunes, 11 de noviembre de 2013

NcN PreQuals 2013: Reto 3 (a)

En el último par de días habíamos hablado de la solución al Reto 1 y Reto 2 de las Quals del CTF de la NcN 2013, así que hoy nos toca ver la solución al Reto 3, el último en el que consistian las Quals. El reto consistía en un binario del que a priori no se proporcionaba más información.

En estos casos, lo mejor es empezar y saber ante que tipo de binario nos encontramos. Si no tiene la cabecera corrupta ni nada similar, debería bastarnos con usar el comando "file":

$ file level.elf 
level.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xb589d432799bf15343387fea63d4bdc00faa177c, not stripped

Ya sabemos algo sobre el binario, es un ELF de 64 bits, así que deberíamos buscarnos un Linux de 64 bits en el que correr este binario ¿No tienes ninguno a mano? En ese caso te pasó igual que a mi al resolver este reto :) Por suerte Linux 64 bits es algo que podemos descargar facilmente de Internet.

Una vez que ya tenemos un Linux de 64 bits y podemos ejecutar el binario, a mi siempre me gusta ejecutarlo una vez a ver lo que hace antes de meterte "a saco" a desensamblar. El funcionamiento del binario te puede dar pistas sobre lo que tienes que buscar en el código.

$ ./level.elf
|  >  Type to win, only what I want to read...
|  > f
|
|  -> I DON'T THINK SO

Cuando llamas al binario, se queda esperando a que pulses alguna tecla y, si es incorrecta, inmediatamente cierra la ejecución. Es de suponer que al introducir la clave correcta nos dará el deseado flag.

Antes de seguir, solo por si fuera así de sencillo, deberíamos hacerle un "strings" al binario, no sea que la contraseña aparezca directamente:

$ strings level.elf
/lib64/ld-linux-x86-64.so.2
libc.so.6
fflush
puts
putchar
[..]
795ef9462825a6640f9a9d7e85a01f5bb311c5849f8fc0e41bf4030f43e583f3
50a89ea2b500750f9baa163adee2057881b418e47bce9ddced4941556614e499
be0978a13b5875b112bcc84b4b688d68ec2d11010543189540bffa3641f2623d
911106de1b05db8d1b0fb2fe118345b4db6ddb930cf61290fd0b8336ffb394fd
a750b2d4129207fbbe6527ddd396f24327f4302c0b8496e154f139612bfc3312
a3f0e19a20d10cb055fbaaf4bbe82859074e50f4f8c2cde2b907c0947941ec98
[...]

No os pongo toda la salida, por no hacer grande el post, pero no vemos nada que pueda parecer la clave ni el flag, pero sí que vemos algo que tiene pinta de estar cifrado. Es muy común en este tipo de retos que haya información cifrada que el propio binario se encargue descifrar, para evitar que sea tan fácil sacar la clave.

Esta vez no hubo suerte con el strings, así que vamos a tener que ejecutar el binario para que haga su trabajo. Otra de las cosas que suelo hacer es usar el comando "ltrace" (o "strace", o "ptrace", según la ocasión). El comando "ltrace" nos mostrará las llamadas a librerías, con lo que si se ejecuta un strcmp() o similar debería aparecernos:

$ ltrace ./level.elf 
__libc_start_main(0x40101f, 1, 0x7fff991818e8, 0x4011c0, 0x401250
fputc('\n', 0x7f279f827260
)                                                                      = 10
fwrite("|  >  ", 1, 6, 0x7f279f827260)                                                           = 6
fflush(0|  >  )                                                                                        = 0
fwrite("Type to win, only what I want to"..., 1, 41, 0x7f279f827260)                             = 41
fflush(0Type to win, only what I want to read... )                                                                                        = 0
fputc('\n', 0x7f279f827260
)                                                                      = 10
fwrite("|  >  ", 1, 6, 0x7f279f827260)                                                           = 6
fflush(0|  >  )                                                                                        = 0
tcgetattr(0, 0x00603400)                                                                         = 0
tcsetattr(0, 0, 0x00603440)                                                                      = 0
getchar(0, 21505, 51729, -1, 0x603440)                                                           = 120
tcsetattr(0, 0, 0x00603400)                                                                      = 0
fputc('\n', 0x7f279f827260
[...]

Tampoco aparece nada, así que una de dos, o el programador está comparando carácter a carácter (algo coherente con el funcionamiento que hemos visto antes) o se ha implementado su propia versión de comparador de cadenas. Vamos a suponer lo primero y vamos a hacer una pequeña prueba: Vamos a hacer un pequeño script en python que pruebe todos los posibles valores ascii para el primer elemento, y vamos a pegar un vistazo a ojo a ver si hay algún cambio en la salida:

$ cat test.py 
#!/usr/bin/python
import os
for c in xrange(1,255):
cmd = 'echo '+str(c)+' ; echo "'+str(chr(c))+'" | ./level.elf'
os.system(cmd)

$ python test.py
[...]
31
|  >  Type to win, only what I want to read... 
|  >  
|
|  -> I DON'T THINK SO
32
|  >  Type to win, only what I want to read... 
|  >  *
|
|  -> I DON'T THINK SO
[...]

Vaya! Parece que sí que hay algo que cambia! Cuando probamos con el carácter 32 (0x20, vamos, el espacio) la salida nos marca con un asterisco, como señalando que hemos marcado un carácter bien, aunque nos sigue diciendo "I DON'T THINK SO", que nos hace ver que aún nos quedan caracteres por meter. Asumiendo que cada vez que acertemos otro carácter tendremos un asterisco más, y que esa frase de "I DON'T THINK SO" desaparecerá una vez obtengamos la solución, nos hacemos otro script en python para que nos automatice un poco la tarea:

$ cat bruteforce.py
#!/usr/bin/python
import subprocess
password = ""
for l in xrange(1,20):
        for c in xrange(1,255):
                if c == 34:
                        continue
                temp_pass = password + chr(c)
                result = subprocess.check_output('echo \"'+temp_pass+'\" | ./level.elf', shell=True)
                if "I DON'T THINK SO" not in result:
                        print temp_pass
                        print result
                        exit(0)
                n = result.count('*')
                if n == l:
                        print temp_pass
                        password = password + chr(c)
                        break

Hacemos un bucle de longitudes de 1 a 20 (podríamos poner más), y para cada posición probamos todos los posibles valores ascii y analizamos el resultado. Si la cantidad de caracteres asterisco es igual a la longitud de la clave que habíamos probado, es que es que hemos acertado, y sino tenemos que probar con el siguiente. Además, si la frase "I DON'T THINK SO" desaparece del resultado, probablemente hemos encontrado la contraseña correcta y tendremos la solución. Vamos a ver si funciona:

$ time ./bruteforce.py
 S
 SU
 SUR
 SURP
 SURPR
 SURPRI
 SURPRIS
 SURPRISE
 SURPRISE!
|  >  Type to win, only what I want to read...
|  >  **********
|
|  -> Congratulations! The key is:
|  9e0d399e83e7c50c615361506a294eca22dc49bfddd90eb7a831e90e9e1bf2fb

real    0m7.179s
user    0m0.772s
sys     0m4.316s

Bingo! Parece que ya tenemos el tercer flag :)
Este reto había muchas maneras de resolverlo. En este caso, como no soy persona que se dedique a hacer reversing a diario, me resultaba más cómoda esta aproximación, pero quise probar también a ver como lo podría haber resuelto arrancando el debugger y viéndole las tripas al binario, por eso he llamado a este post "a", porque falta la solución "b", con debugger.

5 comentarios :

RoMaNSoFt dijo...

Sí que se podía resolver con un strings :)

roman@int3pids:~/int3pids$ strings -eL level.elf
SURPRISE!
roman@int3pids:~/int3pids$

Jose Selvi dijo...

Que buena @RoMaNSoFt :)

El strings de mi Mac no tiene esas opciones.
La verdad es que suelo usar el srch_strings que ya te saca todas las posibles maneras de sacar strings, pero no sé por qué esa vez usé el strings "normal" :P

¿El espacio de delante te lo sacaba también?

Gracias por el aporte!

RoMaNSoFt dijo...

Lo del espacio no lo recuerdo, es posible que no. De todas formas, esto es anecdótico: nosotros no lo resolvimos con strings, de esto me dí cuenta luego a posteriori.

Yo uso el strings de Linux. Por cierto, el srch_strings (Sleuthkit) creo que tampoco te lo saca...

Anónimo dijo...

¿Algún link para descargar ese binario?

Anónimo dijo...


Lo encontre por aqui:
http://foosec.com/docs/mirrors/ctf.noconname.org/index.html