Desarrolla tus propias herramientas: “bruteforce”. 1. Responsabilidad El autor declina toda responsabilidad sobre cualquier uso de la información presentada en el mismo. 2. Introducción En el presente artículo mostraremos cómo desarrollar una simple herramienta de fuerza bruta en el lenguaje de programación: python (http://www.python.org). Lo llevaremos a cabo paso por paso, incluyendo el código generado en cada etapa. Entre los objetivos del texto, se encuentran (aunque no exclusivamente): Animar al lector a desarrollar sus propias herramientas: aunque a veces, existan otras que podamos aplicar, esto no siempre es así; Además, ganamos en flexibilidad. Entender un algoritmo recursivo simple (http://es.wikipedia.org/wiki/Algoritmo_recursivo): aunque no se trata de un texto sobre programación, dado que utilizar un algoritmo recursivo es un método sencillo para resolver este problema (aunque no necesariamente el más óptimo), aprovechamos para utilizarlo de manera que el lector pueda ver su funcionamiento en un lenguaje como python. Pasar un buen rato: :) 3. Aclaración Qué no pretende este artículo: No, no es un texto dedicado a la programación, ni al estilo programando (para eso existen otros textos). Así que si crees que se puede hacer mejor o más limpio, cualquiera de los pasos, te animo a ello. Sí, existen herramientas que ya hacen lo que este ejemplo y quizá sean más eficientes (aunque aquellas, no las hemos diseñado nosotros por lo que adaptarlas a nuestras necesidades, puede ser complicado). 4. Prerequisitos Se presupone unos conocimientos mínimos de programación. Al menos entender las condiciones lógicas, bucles, etc. Unos conocimientos aunque sean básicos del lenguaje python, son recomendables, si bien, no debería ser complicado para el lector, poder adaptar el código a cualquier otro lenguaje con el que se sienta más cómodo, si lo considera necesario. 5. Primeros pasos, definición del algoritmo a crear Intentaremos plasmar, en lenguaje natural, el objetivo del algoritmo a crear: Se trata de un programa que podamos usar para atacar por fuerza bruta una contraseña, en nuestro ejemplo, utilizaremos un md5 sin sal (“salt”). Dada una serie de caracteres debemos ir probando uno a uno y las combinaciones posibles hasta un límite (anchura de palabra) establecido. Por ejemplo, suponiendo que el rango empezase en la letra “a”, sería algo similar a: a, b, c, d… aa, ab, ac… Decidimos, para nuestro trabajo, limitar la lista de caracteres a aquellos comprendidos entre el carácter ascii(32) y el ascii(127). Obviamente, el lector podrá probar cambiando el rango, por ejemplo limitándolo a letras, números y caracteres especiales comúnmente utilizados, sólo alfanuméricos, etc. 6. Enfoque recursivo Pensamos en la siguiente manera de afrontarlo: partiendo de la cadena vacía (‘’) combinamos esa cadena vacía con cada uno de los caracteres del rango. Comprobamos si esa palabra es la que buscamos y, en caso contrario, ejecutamos el mismo proceso pero ahora partiendo con nuestra nueva palabra en lugar de vacío. En caso de que nuestro proceso intente comprobar una palabra con un ancho mayor al límite establecido, terminaremos. Esta definición recursiva, se puede expresar de la siguiente manera: Caso Base: Si ancho de palabra es mayor que el límite => termina Caso Recursivo: Para cada carácter dentro del rango definido: o nuevaBase = base + carácter o verifica(nuevaBase) o Si no es correcto: combina(nuevaBase, ancho + 1) Para simplificar, de momento dejaremos como algoritmo para verificar (check) el código necesario para imprimir la cadena a probar (así podemos comprobar que se está ejecutando correctamente). Nuestro algoritmo recursivo se llamará combineChars y lo llamaremos, para comenzar con una cadena vacía y un límite de 2 caracteres: charRange = range(32,127) def check(string): print string return False # Caso base: # -SI ancho a buscar > limite => Termina. (devuelve None) # Caso Recursivo: # -Para cada caracter posible # -SI Comprueba nuevaBase (base + caracter) == true => termina (devuelve nuevaBase) # -SI NO => Combina(nuevaBase, ancho + 1) def combineChars(base, width, maxLenght): if width > maxLenght: return for char in charRange: newBase = base + chr(char) if check(newBase): print "Encontrado: " + newBase else: combineChars(newBase, width + 1, maxLenght) combineChars('', 1, 2) Si lo ejecutamos veremos que obtenemos la lista de cadenas a probar: ! " # $ % & ' (…) ~| ~} ~~ El siguiente paso que queremos dar es mejorar nuestro sistema de chequeo, lógicamente este procedimiento dependerá del objetivo (podría tratarse de una petición web que analizaríamos para comprobar un ataque de inyección ciega (SQL, XPath…), ataque por fuerza bruta a un md5 o a un campo contraseña, etc). Como comentamos al principio, en nuestro ejemplo usamos un md5 sin “salt”. import hashlib def check(string): print "Probando: " + string + " \t" + hashlib.md5(string).hexdigest() if (searchHash == hashlib.md5(string).hexdigest()): return True return False Añadimos la cadena objetivo y, también, para evitar que el algoritmo continúe verificando otras ramas, cuando ya haya encontrado la palabra a buscar, ejecutamos un sys.exit() (que aunque no es lo más limpio, para nuestro ejemplo sobra. El lector podrá adaptarlo a sus necesidades fácilmente si requiere que no pare la ejecución). El código queda de la siguiente manera: import hashlib import sys charRange = range(32,127) searchHash = "187ef4436122d1cc2f40dc2b92f0eba0" #ab def check(string): print "Probando: " + string + " \t" + hashlib.md5(string).hexdigest() if (searchHash == hashlib.md5(string).hexdigest()): return True return False def combineChars(base, width, maxLenght): if width > maxLenght: return for char in charRange: newBase = base + chr(char) if check(newBase): print "Encontrado: " + newBase sys.exit() else: combineChars(newBase, width + 1, maxLenght) combineChars('', 1, 3) Lo ejecutamos con como límite 3 caracteres y obtenemos el siguiente resultado (en esta ocasión, lógicamente, tardará más): (…) Probando: aaz Probando: aa{ Probando: aa| Probando: aa} Probando: aa~ Probando: ab Encontrado: ab 18b79ceb98d2309a095a9168c2d48363 d4c50a8452fd9a8cdc2553c1081acbc8 0beeea38b67c5ecfecda20d352c4b121 2dae90b7e75a8489e9a6dc57db9e901b 1896cb9442ee07073c39676b6a88b412 187ef4436122d1cc2f40dc2b92f0eba0 Como retoques finales, eliminamos el texto de la prueba que estamos realizando (para limitar el tiempo perdido), y añadimos una comprobación del tiempo utilizado. El código resultante es el siguiente: import time import hashlib import sys charRange = range(32,127) searchHash = "187ef4436122d1cc2f40dc2b92f0eba0" #ab startTime = time.clock() def check(string): if (searchHash == hashlib.md5(string).hexdigest()): return True return False def combineChars(base, width, maxLenght): if width > maxLenght: return for char in charRange: newBase = base + chr(char) if check(newBase): print "Encontrado: " + newBase print "Tiempo: " + str(time.clock() - startTime) sys.exit() else: combineChars(newBase, width + 1, maxLenght) combineChars('', 1, 3) En nuestra prueba (3 caracteres), obtenemos: Encontrado: ab Tiempo: 1.78526949312 Podemos realizar distintas pruebas para obtener los tiempos requeridos. Por ejemplo, repetimos la prueba con un hash de una palabra de 4 caracteres (“hola”) y un límite de 4; Obtenemos: Encontrado: hola Tiempo: 185.964875678 Para finalizar, animamos al lector a crear variaciones sobre el código: Cambiar el algoritmo de chequeo (check) por otro (sha, petición web a página de autenticación, SQL-i…) Mejorar el código eliminando las salidas mediante sys.exit y el estilo de programación. 7. Sobre el Autor Jesús Arnáiz es Consultor de Seguridad en Chase The Sun S.L., entre otros ha trabajado en proyectos de auditoría y consultoría de seguridad, pruebas e intrusión/hacking ético, análisis forense y cumplimiento normativo.