2011/12/20

TDSL

¿Qué? ¿Yet another acrónimo ofensivo?

No, Test Driven Self Learning, esto es, utilizar TDD para aprender.

Al encarar la escritura de esto, busqué "test driven learning", que por supuesto trajo innumerables referencias, asi que no hay nada nuevo bajo el sol.



Ya me había cruzado con la idea en un curso de hibernate y otro de ruby, en los cual me daban los tests e implementaciones parciales y para hacer pasar los test, tenía que implementar el código y de paso aprender. Es realmente un buen método.



Ahora bien, ¿qué pasa cuando no existe el curso? Me hallo en la situación de tener que aprender un nuevo lenguaje, tecnología e incluso agregaría paradigma, sin tener a ninguna persona real o virtual conocida que me asista. Así que decidí ir generando yo mismo los tests, como si aprender no fuera el objetivo.

El proyecto tiene que ver con obtener contraseñas a partir de una base de datos. Veamos primero que es lo que podemos encontrar, como pueden estar almacenadas:

  • En texto plano: listo, no hay nada más que hacer.
  • Hasheada: se le ha aplicado una transformación tal que no sabemos la clave.
  • Hasheada con salt: al método anterior se agrega un valor arbitrario y conocido, ya veremos para que.
Para entrar en contexto, el mecanismo de autenticación suele funcionar asi:
  1. El usuario se identifica y provee su clave
  2. Se busca al usuario en la base de datos
  3. Se aplica una transformación a la clave y se controla que el resultado coincida con lo almacenado. Esta transformación puede ser nula, cuando se ha almacenado la clave en texto plano.

Contra un sistema de autenticación hay varios métodos de ataque, algunos son:

  • Online: se prueban diversas claves hasta acertarle utilizando el sistema. Este puede detectar los intentos y bloquear o limitar el acceso.
  • Ingeniería Social: se obtiene la clave directamente del usuario mediante algún tipo de engaño, que va desde tan sencillo como pedírsela directamente como hacerle entrar en un sitio falso.
  • Offline: se obtiene la base de datos y se prueba en la tranquilidad del hogar.

El último método es el que más me interesa ahora, ya que de eso se trata el proyecto. Una suerte de framework para atacar offline eficientemente una base de datos y que eventualmente facilite un ataque personalizado.


Dado un par(usuario,hash), entonces el objetivo es recuperar la contraseña. Los pasos pueden ser:
  1. Buscar hash en internet, si esto falla, continuar.
  2. Determinar que tipo de hash es.
  3. Determinar si y cual salt puede haber.
  4. Reproducir el algoritmo que lo genera
  5. Utilizar una lista de claves frecuentes, aplicar el algoritmo y ver si le pegamos.
(Para ser estricto, tras el paso 3 se podrían usar rainbow tables, pero eso lo dejamos para otro dia, ¿ok?)
    Para el primer paso contamos con los buscadores. A mano he llegado a lograr que google me confunda con un bot y me tire un captcha. Esto no escala, pues aunque de 400 entradas que me restaban de un ataque de 800 pude recuperar 60, me llevó un par de horas, mias, no va.

    ¿Por qué, Charli, que sos tan basher no hiciste un for HASH in $(cut users.txt -d" " -f 3); do wget http://www.google.com/search?q=$HASH -O $HASH.html; done?

    Pues lo hice, pero google me bloqueaba en la primera, supongo que por el user-agent. ¿Sabés que wget puede impersonar cualquier user-agent? Si, pero quería analizar bien los resultados y cuando llegue a 30 o 40, tras 200 intentos, ya fue. Igual iba a tener que inspeccionar los *.html de modo un tanto manual, otro dia será.

    Para determinar el tipo de hash, aprovechamos el paso anterior, en alguna página donde hubo acierto va a decir que tipo es.

    Si hubo salt involucrada la cosa se complica. Lo mejor es tener la base de datos, el código y la configuración, de ahi sale, sino... Ahora es el despues de "ya veremos para que".

    Siempre se deben guardar las claves hasheadas utilizando salt
    • Si no se hashea, se obtiene el 100% de las claves

    • Si se hashea, se obtiene el 99% de las claves débiles como "123456", "maradona", "c@Rl05", "pablo1974", "bugzilla", "klingon", "marsupilami", "newyork", "Oberon". A fines prácticos cerca del %50 del total.

    • Si se usa salt aunque esa salt caiga en manos del atacante, deshabilita buscar en internet y dificulta rainbow tables

    Ok, voy a explicar que son rainbow tables: ¿recuerdan el quinto paso, el de probar las palabras del diccionario contra cada clave? Es algo parecido, pero en lugar de probar y tirar el resultado que no sirve, se generan y guardan todos los hashes de un set para cada algoritmo. El set esta generado por ejemplo por todas las palabras posibles de una a ocho letras combinación de "a-zA-Z0-9". Esto lleva mucho tiempo, ocupa mucho espacio pero luego permite recuperar muy rápido. Más detalles en la internet. Ah, ¿pero por qué un hash interfiere con rainbow tables? Si a tu tonta clave "123456" antes de hashearla le agregás "hola" tal que te quede "123456hola" ahora mide diez y no está en la rainbow table.

    Si el atacante consigue la salt "hola", debe generar todas sus rainbow tables para esa salt. Esto es, debe generar las tablas para cada sitio que ataque. Si además le agregas una "salt local" (como yo la llamo en contraposición a la "salt global") tal que te quede "123456hola#{userid}", tiene que generar la rainbow table para cada usuario. Esto no imposibilita el ataque, pero lo dificulta. Si es la cuenta de alguien importante, mas vale que ponga una clave larga.


    Tras el rant, sigamos con reproducir el algoritmo. Es bueno tener el código fuente. Ah, ¿dije algo obvio? Si no, un par (usuario, clave) conocido en la base para utilizar como referencia, para usar algorimos equivalentes quizas en otro lenguaje.

    El problema es el último paso. En una ocasión, 1000 usuarios con un diccionario de 68.000.000 de claves calculo que me hubiera llevado dos dias. Dije "calculo" y ahi esta la clave de todo este asunto, pues como tenía 4+2 cores a mi disposición, de los cuales usé 3+1, pude paralelizar y me llevó medio dia. It's a bash of magic.

    Ahora entra erlang en la ecuación.

    No voy a repetir la marketinera introducción a erlang, podés leerla directamente de las fuentes. Tratándose erlang de un lenguaje orientado a la concurrencia y a ser distribuido, puede ser una buena elección, pero tengo que convertir mis scripts en bash, php, ruby y perl en algo coherente.

    Comienzo con el de php, que lo que hace es convertir "hola" en "h01@", "h014", "h01A", "h01a", "h0|@", "h0|4", "h0|A", "h0|a", "h0L@", "h0L4", "h0LA", "h0La", "h0l@", "h0l4", "h0lA", "h0la", "h@1@", "h@14", "h@1A", "h@1a", "h@|@", "h@|4", "h@|A", "h@|a", "h@L@", "h@L4", "h@LA", "h@La", "h@l@", "h@l4", "h@lA", "h@la", "hO1@", "hO14", "hO1A", "hO1a", "hO|@", "hO|4", "hO|A", "hO|a", "hOL@", "hOL4", "hOLA", "hOLa", "hOl@", "hOl4", "hOlA", "hOla", "ho1@", "ho14", "ho1A", "ho1a", "ho|@", "ho|4", "ho|A", "ho|a", "hoL@", "hoL4", "hoLA", "hoLa", "hol@", "hol4", "holA", "hola", "H01@", "H014", "H01A", "H01a", "H0|@", "H0|4", "H0|A", "H0|a", "H0L@", "H0L4", "H0LA", "H0La", "H0l@", "H0l4", "H0lA", "H0la", "H@1@", "H@14", "H@1A", "H@1a", "H@|@", "H@|4", "H@|A", "H@|a", "H@L@", "H@L4", "H@LA", "H@La", "H@l@", "H@l4", "H@lA", "H@la", "HO1@", "HO14", "HO1A", "HO1a", "HO|@", "HO|4", "HO|A", "HO|a", "HOL@", "HOL4", "HOLA", "HOLa", "HOl@", "HOl4", "HOlA", "HOla", "Ho1@", "Ho14", "Ho1A", "Ho1a", "Ho|@", "Ho|4", "Ho|A", "Ho|a", "HoL@", "HoL4", "HoLA", "HoLa", "Hol@", "Hol4", "HolA", "Hola" y es llamado mediante un vulgar system/exec desde perl y ruby (bugzilla y redmine respectivamente).

    Esta función que sorpresivamente se llama leet(), me costó bastante, ya que la solución es recursiva. ¡Qué bien! Erlang es funcional (o sea que todo se hace con recursividad), no debería ser tan terrible, ¿no?

    Varios dias despues:

    De modo colateral he hallado que erlang soluciona todas esas discusiones de como testear los métodos privados que asolan las listas de discusión ágiles de un modo muy sencillo.

    La unidad de compilación es el módulo, que podría ser asi:

    -module(cracker).
    -export([leet/1]).

    leet(Palabra) ->...

    funcion_interna() -> ok.

    otra_funcion_interna() -> ok.

    Los más sagaces podrán inferir que desde afuera se puede acceder a leet/1 y no asi a las funciones internas. Salteemonos un paso y veamos el código que prueba esto:

    -module(cracker_tests).
    -include_lib("eunit/include/eunit.hrl").

    leet_test()->?assertEqual(..,cracker:leet("hola")).

    otro_test()->?asserEqual(ok,cracker:funcion_interna()).

    ¡No! ¡A los sagaces nos diste a entender que el módulo cracker_test no puede ver a las funciones que no han sido exportadas!

    Es parcialmente verdad, si desde el interprete (erl), compilamos normal.

    1> c("src/cracker").
    {ok,cracker}
    2> cracker:otra_funcion().
    ** exception error: undefined function cracker:otra_funcion/0


    pero si pasamos la opción "export_all", vemos todo, testeamos todo.

    3> c("src/cracker", [export_all]).
    {ok,cracker}
    4> cracker:otra_funcion().
    ok



    Sin tocar el código, sin agregar funciones,  sin afectar nuestra política de lo que es público y lo que es privado.

    Sigo cuando logre implementar leet/1.

    No hay comentarios:

    Publicar un comentario