domingo, 4 de octubre de 2015

Solución automática de crackmes

Hay dos cosas que he aprendido de los equipos que frecuentemente ganan en los CTF:
  1. Es una ventaja tener jugadores distribuidos en diversas zonas horarias. De esta forma, mientras unos duermen los otros juegan, y el equipo no tiene tiempos muertos en el scoreboard.
  2. Es una necesidad automatizar todo lo que sea automatizable.  De esta forma, el equipo puede dedicar su tiempo a los niveles que no pueden explotarse de forma automática.
La idea de este post es mostrar una de las técnicas que existen para automatizar la solución de algunos crackmes.  Específicamente, algunos de los crackmes en los que la validación del serial es un conjunto de pequeñas validaciones.  Por ejemplo, un algoritmo como el siguiente:
  • Si la longitud del serial es menor a seis caracteres, falla la validación y no se verifica nada mas
  • Si el primer caracter no es una 'R', falla la validación y no se verifica nada mas
  • Si el cuarto caracter no es un número menor que 4, falla la validación y no se verifica nada mas.
  • Si el sexto caracter no es igual al segundo caracter, falla la validación y no se verifica nada mas.

O alguna otra cosa por el estilo, donde se mantiene ese patrón de saltar rápidamente al final (y ya no seguir validando) si alguna validación intermedia falla.

Un ejemplo bastante familiar sería el caso de strcmp (donde primero se verifica si el primer caracter de las cadenas a comparar es igual y si no lo es terminamos, pero si es igual verificamos otro caracter, y luego otro, y otro hasta encontrar alguno diferente o hasta terminar de recorrer las cadenas).  Desde luego, algo tan simple como un strcmp seguramente se resuelve mas fácil con ltrace, pero esta técnica tambien aplica.

La técnica que veremos está descrita en "A binary analysis, count me if you can" (Salwan, 2013) y se basa en usar la herramienta "pin" (el framework de instrumentación binaria dinámica desarrollado por Intel) para calcular el conteo de instrucciones ejecutadas por el binario que estamos analizando y desde allí inferir si algún cambio en nuestro serial nos acerca o no a la solución.  Convenientemente, la herramienta "pin" incluye un ejemplo para calcular los conteos de instrucciones :)

Podemos descargar la herramienta pin desde aquí y proceder a compilar el pintool "inscount0.so":


user@debian:~/reversing$ wget --quiet http://software.intel.com/sites/landingpage/pintool/downloads/pin-2.14-71313-gcc.4.4.7-linux.tar.gz

user@debian:~/reversing$ tar xf pin-2.14-71313-gcc.4.4.7-linux.tar.gz

user@debian:~/reversing/pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples$ cd pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/

user@debian:~/reversing/pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples$ make inscount0.test
g++ -DBIGARRAY_MULTIPLIER=1 -Wall -Werror -Wno-unknown-pragmas -fno-stack-protector -DTARGET_IA32 -DHOST_IA32 -DTARGET_LINUX  -I../../../source/include/pin -I../../../source/include/pin/gen -I../../../extras/components/include -I../../../extras/xed-ia32/include -I../../../source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing   -c -o obj-ia32/inscount0.o inscount0.cpp
g++ -shared -Wl,--hash-style=sysv -Wl,-Bsymbolic -Wl,--version-script=../../../source/include/pin/pintool.ver    -o obj-ia32/inscount0.so obj-ia32/inscount0.o  -L../../../ia32/lib -L../../../ia32/lib-ext -L../../../ia32/runtime/glibc -L../../../extras/xed-ia32/lib -lpin -lxed -lpindwarf -ldl
make -C ../../../source/tools/Utils dir obj-ia32/cp-pin
make[1]: se ingresa al directorio `/home/user/reversing/pin-2.14-71313-gcc.4.4.7-linux/source/tools/Utils'
mkdir -p obj-ia32
g++  -DTARGET_IA32 -DHOST_IA32 -DFUND_TC_TARGETCPU=FUND_CPU_IA32 -DFUND_TC_HOSTCPU=FUND_CPU_IA32 -DTARGET_LINUX -DFUND_TC_TARGETOS=FUND_OS_LINUX -DFUND_TC_HOSTOS=FUND_OS_LINUX  -O3   -o obj-ia32/cp-pin cp-pin.cpp
make[1]: se sale del directorio `/home/user/reversing/pin-2.14-71313-gcc.4.4.7-linux/source/tools/Utils'
../../../pin   -t obj-ia32/inscount0.so -- ../../../source/tools/Utils/obj-ia32/cp-pin makefile obj-ia32/inscount0.makefile.copy \
          > obj-ia32/inscount0.out 2>&1
cmp makefile obj-ia32/inscount0.makefile.copy
rm obj-ia32/inscount0.makefile.copy
rm obj-ia32/inscount0.out
user@debian:~/reversing/pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples$ cd ../../../..

user@debian:~/reversing$ 

Estamos listos!

Caso 1: DrSpliff's drscm3

Empecemos analizando el crackme "DrSpliff's drscm3":

user@debian:~/reversing$ ./crack
User ID: rmolina
Error: Please enter a number between 0x01 and 0xFF
user@debian:~/reversing$

El primer paso es hallar el valor "User ID" correcto.  Como solo son 255 posibles valores podemos probarlos todos.  Aquí esta la salida (truncada):

user@debian:~/reversing$ for i in `seq 255`; do echo -n "$i: "; ./crack <<< $i; done
1: Violación de segmento
2: Violación de segmento
3: Violación de segmento
4: Violación de segmento
5: Violación de segmento
6: Violación de segmento
7: Violación de segmento
8: Violación de segmento
9: Violación de segmento
10: Violación de segmento
11: Violación de segmento
12: Violación de segmento
13: Violación de segmento
14: Violación de segmento
15: Violación de segmento
16: Violación de segmento
17: Violación de segmento
18: Violación de segmento
19: Violación de segmento
20: Violación de segmento
21: Violación de segmento
22: Violación de segmento
23: Violación de segmento
24: Violación de segmento
25: Violación de segmento
26: Violación de segmento
27: Violación de segmento
28: Violación de segmento
29: Violación de segmento
30: Violación de segmento
31: Violación de segmento
32: Violación de segmento
33: Violación de segmento
34: Violación de segmento
35: Violación de segmento
36: Violación de segmento
37: Violación de segmento
38: Violación de segmento
39: Violación de segmento
40: Violación de segmento
41: Violación de segmento
42: Violación de segmento
43: Violación de segmento
44: Violación de segmento
45: Violación de segmento
46: Violación de segmento
47: Violación de segmento
48: Violación de segmento
49: Violación de segmento
50: Instrucción ilegal
51: Violación de segmento
52: Violación de segmento
53: Violación de segmento
54: Violación de segmento
55: Violación de segmento
56: Violación de segmento
57: Violación de segmento
58: `trap' para punto de parada/seguimiento
59: Violación de segmento
60: Violación de segmento
61: Violación de segmento
62: Violación de segmento
63: Violación de segmento
64: Violación de segmento
65: Violación de segmento
66: Violación de segmento
67: Violación de segmento
68: Violación de segmento
69: Instrucción ilegal
70: Violación de segmento
71: Violación de segmento
72: Violación de segmento
73: Violación de segmento
74: Violación de segmento
75: Violación de segmento
76: Violación de segmento
77: Violación de segmento
78: Violación de segmento
79: Violación de segmento
80: Violación de segmento
81: Violación de segmento
82: Violación de segmento
83: Violación de segmento
84: Violación de segmento
85: Violación de segmento
86: Violación de segmento
87: Violación de segmento
88: Violación de segmento
89: Violación de segmento
90: Violación de segmento
91: Violación de segmento
92: Violación de segmento
93: Violación de segmento
94: Violación de segmento
95: Instrucción ilegal
96: Violación de segmento
97: Violación de segmento
98: Violación de segmento
99: Violación de segmento
100: Violación de segmento
101: Violación de segmento
102: Violación de segmento
103: Violación de segmento
104: Violación de segmento
105: Violación de segmento
106: Violación de segmento
107: Violación de segmento
108: Instrucción ilegal
109: Violación de segmento
110: Violación de segmento
111: Violación de segmento
112: Violación de segmento
113: Violación de segmento
114: Violación de segmento
115: Violación de segmento
116: Violación de segmento
117: Violación de segmento
118: Violación de segmento
119: Violación de segmento
120: Violación de segmento
121: Violación de segmento
122: Violación de segmento
123: Lice%
Oops!

Confirmamos que al ingresar "123" nos solicita un numero de licencia:

user@debian:~/reversing$ ./crack
User ID: 123
Lice% rmolina

Oops!
user@debian:~/reversing$

En este punto podemos verificar el comportamiento usando el pintool:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack; cat inscount.out
User ID: 123
Lice% a

Oops!
Count 80753

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack; cat inscount.out
User ID: 123
Lice% b

Oops!
Count 80753

user@debian:~/reversing$

Si continuamos probando con caracteres distintos, eventualmente encontramos un caracter para el que el conteo de instrucciones cambia:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack; cat inscount.out
User ID: 123
Lice% 5

Oops!
Count 80753

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack; cat inscount.out
User ID: 123
Lice% 6

Oops!
Count 80753

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack; cat inscount.out
User ID: 123
Lice% 7

Oops!
Count 80760
user@debian:~/reversing$

Concluimos que el serial comienza por "7" :)

En este punto podemos automatizar el proceso.  Aquí esta mi script:

import pexpect
import time
import re
import string


cmd = "./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./crack"


def send_password(password):
    child = pexpect.spawn(cmd)
    child.expect('User ID:')
    child.sendline('123')
    child.expect('Lice%')
    child.sendline(password)

    while True:
        with open('inscount.out', 'r') as f:
            count = re.findall('Count (.*)', f.read())
        if len(count):
            count = count.pop()
            break

    return count


def search_one(known = ''):
    old_char = None
    old_count = None
    for new_char in string.printable:
        new_count = send_password(known + new_char)
        if old_count is not None and new_count != old_count:
            if new_count > old_count:
                return known + new_char
            else:
                return known + old_char
                #return -1
            break
old_count = new_count old_char = new_char return -1 known = '' while True: known = search_one(known) if known == -1: break else: print "Found:", known

Aquí esta la salida del script:

user@debian:~/reversing$ time python crack.py
Found: 7
Found: 7s
Found: 7sd
Found: 7sd9
Found: 7sd9g
Found: 7sd9gw
Found: 7sd9gwi
Found: 7sd9gwig
Found: 7sd9gwigE

real    4m29.162s
user    0m8.733s
sys     3m39.602s
user@debian:~/reversing$

Confirmamos el serial que hemos encontrado:

user@debian:~/reversing$ ./crack
User ID: 123
Lice% 7sd9gwigE

Thanks!
user@debian:~/reversing$

Confirmado :)

Caso 2: rezk2ll's BeatME

Probaremos ahora con un keygenme: "rezk2ll's BeatME".  Desde luego, no podemos obtener el algoritmo de generación de claves usando solo esta técnica, así pues, el objetivo será obtener el password válido para el usuario "rmolina".

user@debian:~/reversing$ ./BeatMe
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : rmolina
PASSWORD : rmolina
NOPE , YOU LOSE
user@debian:~/reversing$

Si modificamos el script anterior para usar "USERNAME" y "PASSWORD" en lugar de "User ID" y "Lice%", veremos que no logra resolver el problema, y se queda atrapado en un loop por mucho tiempo (espere cerca de 15 minutos antes de matar el proceso al no ver ningun progreso).

Podemos entonces verificar manualmente si el binario requiere una longitud mínima del usuario:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : a
NOPE , YOU LOSE
Count 1348

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aa
NOPE , YOU LOSE
Count 1348

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aaa
PASSWORD : a
NOPE , YOU LOSE
Count 1438

user@debian:~/reversing$

Necesitamos un mínimo de tres caracteres en el usuario para que el crackme nos pida un password.   Verificamos entonces la longitud mínima del password:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aaa
PASSWORD : a
NOPE , YOU LOSE
Count 1438

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aaa
PASSWORD : aa
NOPE , YOU LOSE
Count 1438

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aaa
PASSWORD : aaa
NOPE , YOU LOSE
Count 1438

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe; cat inscount.out
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : aaa
PASSWORD : aaaa
NOPE , YOU LOSE
Count 1449

user@debian:~/reversing$

Ya que el conteo de instrucciones cambia de 1438 a 1449, concluimos que se necesita un mínimo de cuatro caracteres en el password :)

Podemos entonces actualizar nuestro script inicial para completar los passwords a un mínimo de 3 caracteres (Sólo es necesario cambiar unas pocas líneas: las del expect, el nombre del crackme, y el segundo return):

import pexpect
import time
import re
import string


cmd = "./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./BeatMe"

def send_password(password):
    child = pexpect.spawn(cmd)
    child.expect('USERNAME')
    child.sendline('rmolina')
    child.expect('PASSWORD')
    child.sendline(password + 'A' * (4 - len(password)) )

    while True:
        with open('inscount.out', 'r') as f:
            count = re.findall('Count (.*)', f.read())
        if len(count):
            count = count.pop()
            break

    return count


def search_one(known = ''):
    old_char = None
    old_count = None
    for new_char in string.printable:
        new_count = send_password(known + new_char)
        if old_count is not None and new_count != old_count:
            if new_count > old_count:
                return known + new_char
            else:
                return known + old_char
                #return -1
            break
        old_count = new_count
        old_char = new_char
    return -1


known = ''
while True:
    known = search_one(known)
    if known == -1:
        break
    else:
        print "Found:", known

Y esta es la salida:
user@debian:~/reversing$ time python BeatMe.py
Found: 7
Found: 7o
Found: 7ov
Found: 7ovq
Found: 7ovqs
Found: 7ovqsp
Found: 7ovqspm
Found: 7ovqspmr
Found: 7ovqspmre

real    3m52.945s
user    0m4.728s
sys     2m58.279s
user@debian:~/reversing$

Confirmamos el serial que hemos encontrado:

user@debian:~/reversing$ ./BeatMe
 ____             _   __  __
| __ )  ___  __ _| |_|  \/  | ___
|  _ \ / _ \/ _` | __| |\/| |/ _ \
| |_) |  __/ (_| | |_| |  | |  __/
|____/ \___|\__,_|\__|_|  |_|\___| | ReZK2LL

USERNAME : rmolina
PASSWORD : 7ovqspmre
CORRECT , YOU WIN
user@debian:~/reversing$

Confirmado :)

Caso 3: jockcranley's T0AD K3YG3N


Vamos ahora a probar otro keygenme: "jockcranley's T0AD K3YG3N".  Nuevamente, buscaremos el serial del usuario "rmolina":

user@debian:~/reversing$ ./toadkey32
< T0AD K3YG3N >
Username: rmolina
Password: rmolina
Access Denied.
user@debian:~/reversing$

Exploramos con el pintool la longitud mínima del password:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: a
Access Denied.
Count 102306

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: aa
Access Denied.
Count 102315

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: aaa
Access Denied.
Count 102324

user@debian:~/reversing$

Vemos que el conteo va incrementando conforme agregamos otro caracter.  Si continuamos un poco mas veremos:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: aaaaaaaa
Access Denied.
Count 102362

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: aaaaaaaaa
Access Denied.
Count 102362

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32 ; cat inscount.out
< T0AD K3YG3N >
Username: rmolina
Password: aaaaaaaaaa
Access Denied.
Count 102362

user@debian:~/reversing$

A partir de 8 caracteres, el conteo no cambia. Concluimos que la longitud del password es 8 caracteres :)

Si modificamos el script anterior para usar un mínimo de 8 caracteres en lugar de 4 (y claro, actualizando también el nombre del crackme), veremos que solo se recuperan 4 caracteres antes de terminar:

user@debian:~/reversing$ time python toadkey32.py
Found: D
Found: D@
Found: D@L
Found: D@Lh

real    7m58.693s
user    1m16.029s
sys     6m38.833s
user@debian:~/reversing$


Cambiamos nuevamente el segundo return (para dejarlo como en el caso 1) y de pasada agregamos una condicion de finalizacion en las ultimas lineas:

import pexpect
import time
import re
import string


cmd = "./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/so                                                                                        urce/tools/ManualExamples/obj-ia32/inscount0.so -- ./toadkey32"

def send_password(password):
    child = pexpect.spawn(cmd)
    child.expect('Username')
    child.sendline('rmolina')
    child.expect('Password')
    child.sendline(password + 'A' * (8 - len(password)) )

    while True:
        with open('inscount.out', 'r') as f:
            count = re.findall('Count (.*)', f.read())
        if len(count):
            count = count.pop()
            break

    return count


def search_one(known = ''):
    old_char = None
    old_count = None
    for new_char in string.printable:
        new_count = send_password(known + new_char)
        if old_count is not None and new_count != old_count:
            if new_count > old_count:
                return known + new_char
            else:
                return known + old_char
                #return -1
            break
        old_count = new_count
        old_char = new_char
    return -1


known = ''
while True:
    known = search_one(known)
    if known == -1:
        break
    else:
        print "Found:", known
    if len(known) == 8:
        break

Aquí esta la salida con los tiempos (mucho mas lento que los anteriores):

user@debian:~/reversing$ time python toadkey32.py
Found: D
Found: D@
Found: D@L
Found: D@Lh
Found: D@Lh0
Found: D@Lh0\
Found: D@Lh0\T
Found: D@Lh0\TT

real    14m16.726s
user    2m56.303s
sys     10m44.432s
user@debian:~/reversing$

Y verificamos el resultado:

user@debian:~/reversing$ ./toadkey32
< T0AD K3YG3N >
Username: rmolina
Password: D@Lh0\TT
Access Granted.
user@debian:~/reversing$

Confirmado :)

Caso 4: NobZ's CrackmeLinux

Finalmente, veamos el crackme  "NobZ's CrackmeLinux".  Este no lee el serial desde la entrada estandar sino que lo recibe como argumento (no necesitaremos entonces usar pexpect).

user@debian:~/reversing$ ./CrackmeLinux
-E- Exactly one argument needed
user@debian:~/reversing$

Vamos directamente a explorar la longitud del password:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux a ; cat inscount.out
Try again, bro...
Count 78

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aa ; cat inscount.out
Try again, bro...
Count 79

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaa ; cat inscount.out
Try again, bro...
Count 80

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaaa ; cat inscount.out
Try again, bro...
Count 81

user@debian:~/reversing$

Con cada caracter que agregamos el conteo se incrementa en uno.  Si seguimos incrementando eventualmente veremos lo siguiente:

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaaaaaaaaaaaa ; cat inscount.out
Try again, bro...
Count 90

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaaaaaaaaaaaaa ; cat inscount.out
Try again, bro...
Count 91

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaaaaaaaaaaaaaa ; cat inscount.out
Try again, bro...
Count 184

user@debian:~/reversing$ ./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux aaaaaaaaaaaaaaaa ; cat inscount.out
Try again, bro...
Count 93
user@debian:~/reversing$

Cuando el password alcanza 15 caracteres se ejecutan muchas mas instrucciones que en los casos anteriores.  Ademas, cuando el password tiene 16 caracteres, nuevamente se ejecutan pocas instrucciones.  Concluimos que la longitud es 15 :)

Preparamos el siguiente script (usando subprocess en lugar de pexpect):

import subprocess
import time
import re
import string


cmd = "./pin-2.14-71313-gcc.4.4.7-linux/pin -t pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-ia32/inscount0.so -- ./CrackmeLinux"
def send_password(password):
    password = password.replace("'", "\'")
    password = password.replace('"', '\"')
    child = subprocess.call(cmd + " '" + password + "A" * (15 - len(password)) + "'", shell=True, stdout=subprocess.PIPE)

    while True:
        with open('inscount.out', 'r') as f:
            count = re.findall('Count (.*)', f.read())
        if len(count):
            count = count.pop()
            break

    return count


def search_one(known = ''):
    old_char = None
    old_count = None
    for new_char in string.printable:
        new_count = send_password(known + new_char)
        if old_count is not None and new_count != old_count:
            if new_count > old_count:
                return known + new_char
            else:
                return known + old_char
                #return -1
            break
        old_count = new_count
        old_char = new_char
    return -1


known = ''
while True:
    known = search_one(known)
    if known == -1:
        break
    else:
        print "Found:", known

Y esta es la salida:

user@debian:~/reversing$ time python CrackmeLinux.py
Found: 0
Found: 0b
Found: 0bf
Found: 0bfu
Found: 0bfu5
Found: 0bfu5c
Found: 0bfu5c4
Found: 0bfu5c4t
Found: 0bfu5c4t3
Found: 0bfu5c4t3D
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-_
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-_-
Found: 0bfu5c4t3D=-_-!
/bin/sh: 1: Syntax error: Unterminated quoted string

real    4m24.479s
user    0m4.756s
sys     5m29.233s
user@debian:~/reversing$

Aunque se generan algunos errores, hemos encontrado un password.  Vamos a confirmarlo:

user@debian:~/reversing$ ./CrackmeLinux 0bfu5c4t3D=-_-!
Try again, bro...
user@debian:~/reversing$

Uhm… Algo malo ha pasado aquí.  Veamos de nuevo la salida:

user@debian:~/reversing$ time python CrackmeLinux.py
Found: 0
Found: 0b
Found: 0bf
Found: 0bfu
Found: 0bfu5
Found: 0bfu5c
Found: 0bfu5c4
Found: 0bfu5c4t
Found: 0bfu5c4t3
Found: 0bfu5c4t3D
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-_
/bin/sh: 1: Syntax error: Unterminated quoted string
Found: 0bfu5c4t3D=-_-
Found: 0bfu5c4t3D=-_-!
/bin/sh: 1: Syntax error: Unterminated quoted string

real    4m24.479s
user    0m4.756s
sys     5m29.233s
user@debian:~/reversing$

Cuando comienzan los caracteres especiales empieza el error reportando unas comillas sin cerrar (esto se debe a que la comilla es uno de los caracteres que probamos). Pero hay un caso en el que no se presenta el error, posiblemente porque aparece una comilla adicional que completa el par.  Lo que imagino es que este serial incluye una comilla :S

Probaremos con comillas simples y dobles (escapandolas con la contraria):

user@debian:~/reversing$ ./CrackmeLinux "0bfu5c4t3D=-_-'"
Try again, bro...

user@debian:~/reversing$ ./CrackmeLinux '0bfu5c4t3D=-_-"'
Yeah ! You did it !

Confirmado. El serial terminaba con una comilla doble :)  Este crackme tenía una trampa adicional…

Conclusión

Es cosa de minutos modificar el script en python para hacer la prueba.  Y la ejecución puede tomar algun tiempo tambien, pero es tiempo que podemos aprovechar haciendo otra cosa.  Por lo tanto, aunque este truco sólo funcionará en algunos casos (en los que la validación el serial se va verificando paso a paso y no en bloque), bien vale la pena probar esta técnica durante un CTF :)

Bonus Track :)

Cómo lo mencioné antes, la técnica también funcionaría con un strcmp.  Por ejemplo, este pequeño crackme (que sería trivial resolver con strings o ltrace) también se puede resolver via conteo de instrucciones. pero les dejo de tarea implementarlo :)

#include <stdio.h>
#include <string.h>
#include <readline/readline.h>

int main() {
   static char *serial = (char *)NULL;
   serial = readline("serial: ");
   if (!strcmp(serial, "Secret0")) {
       printf("yes\n");
   } else {
       printf("nop\n");
   }
   return 0;
}

Aquí la salida del script que preparé:

user@debian:~/reversing$ python rmolina.py
Found: S
Found: Se
Found: Sec
Found: Secr
Found: Secre
Found: Secret
Found: Secret0

Toma más tiempo que los anteriores, pero igual funciona :)

Actualización

Escribí un pequeño gist que soluciona los casos basados en pexpect, incluyendo la identificación de la longitud mínima del password.  Tal vez más adelante haga lo mismo para generalizar un poco más los casos basados en subprocess.

No hay comentarios:

Publicar un comentario