13 de Junio, 2010

Guardando contraseñas de forma segura

Post-it stored password

En esta bitácora no es necesario, ni para el único usuario que se autentica (el administrador), porque he usado uno de los módulos de Tornado (concretamente Google OAuth), así que entro en la administración usando mi Google Account. Pero en mi último intento con Dancer y Perl, sí implementé gestión de usuarios (y sus contraseñas).

Recuerdo que en el año 2001 me miraron raro en la empresa donde trabajaba porque decía cosas como cifrar contraseñas usando MD5. Es decir, no guardar las contraseñas en texto plano en la aplicación, para reducir el impacto en caso de que las mismas se vieran comprometidas.

Además poco después implementé contraseñas de un uso con Javascript, para asegurar la autenticación en los casos en los que no disponíamos de SSL y HTTPS, que se basa en la misma idea que voy a explicar hoy.

En aquel momento fue una mejora, pero definitivamente estaba mal implementado, y la demostración es la siguiente:

21232f297a57a5a743894a0e4a801fc3

¿Está claro no? Por si acaso: es el hash MD5 de la palabra admin (una de las contraseñas temporales más usadas, aún reconozco su MD5 cuando lo veo :P).

Almacenar un hash de la contraseña no es seguro, si existe la posibilidad de que haya un DVD circulando por ahí con tablas precalculadas y listas para comparar con nuestra contraseñas cifradas.

No es que MD5 sea vulnerable, de hecho es un hash bastante lento de calcular, lo que lo hace adecuado para entorpecer ataques con fuerza bruta (que hayan n colisiones cada m millones de generaciones, no es tan grave porque no estamos firmando), sino que los usuarios no usan buenas contraseñas, así que podemos preparar descargar una rainbow table que nos facilite el ataque.

Si lo estamos haciendo mal, ¿cuál es la forma adecuada de guardar contraseñas?

En primer lugar, trabajar con cifrado sin tener suficientes conocimientos suele ser el primer paso para dispararse en un pie:

Es más fácil no usar mal un framework bien implementado, que implementar bien nuestra propia rutina criptográfica.

Mi propuesta es aplicar la idea de salt, que ya se empleaba en las versiones iniciales de UNIX para cifrar las contraseñas (¡oh, novedad!).

Ese salt es una serie aletoria de bits que se utiliza para derivar una clave en otra, de forma que el hash obtenido tras cifrar dos veces la misma contraseña, sea diferente.

Mi implementación en Perl está integrada en el ORM, pero refactorizando un poco tenemos:

  • La contraseña almacenada: consiste en 6 bytes de salt, concatenados al hash de la contraseña derivada. A un salt más grande y aleatorio, la fórmula es más robusta. Además es buena idea usar una función de hash lo más lenta posible (entonces: MD5 mejor que SHA).
  • La función para nuevas contraseñas: dada la contraseña de usuario, genera un salt, deriva la contraseña y cifra la contraseña para almacenar.
  • Una función para comparar: toma como entrada una contraseña en plano (la que nos pasa el usuario al autenticarse), y una contraseña almacenada. Su tarea es derivar la contraseña de usuario y generar el hash correspondiente para comparar con lo que tenemos almacenado.

De esta forma conseguimos que los ataques con tablas rainbow sean inviables.

Veamos código de ejemplo:

use strict;
use warnings;

use Digest::MD5 qw(md5_hex);

# salt size in bytes
my $SALT_SIZE = 6;

# params: plain password
# returns: hashed password
sub newPassword
{
    my $plain = shift;
    
    # we get characters with substr, so mult x 2 the number
    # of bytes (1 byte is 2 chars in ascii)
    my $salt = substr(md5_hex(rand()), 0, $SALT_SIZE*2);

    return $salt .md5_hex($salt . $plain);
}

# params: hashed password, plain password
# returns: 1 if are equal
sub cmpHashPasswd
{
    my $hashed = shift;
    my $plain = shift;
    
    # we get characters with substr, so mult x 2 the number
    # of bytes (1 byte is 2 chars in ascii)
    my $salt = substr($hashed, 0, $SALT_SIZE*2);
    my $plainHashed = $salt .md5_hex($salt . $plain);
    
    return $hashed eq $plainHashed;
}

# simple test: admin
my @hashed = ();

for (my $i=0; $i<10; $i++)
{
    push(@hashed, newPassword("admin"));
}

foreach(@hashed)
{
    print "admin hashed as: $_, is admin? "
        .cmpHashPasswd($_, "admin") ."\n";
}

En el ejemplo vemos como generamos 10 contraseñas listas para almacenar, que son distintas (lo que imposibilita un ataque rainbow), y que podemos compararlas con la contraseña en texto plano sin ningún problema.

Pero si volvemos a mi consejo inicial: ¿nos podemos fiar de esta implementación?

En este caso es sencillo, y además me tomé la molestia de documentarme y revisar el código de la implementación de OpenBSD en C, así que a mi me parece de fiar :D. Sino, siempre hay frameworks que lo hacen bien: buscamos uno, lo evaluamos, y listo; pero bajo ningún concepto usaremos un hash sin más para almacenar las contraseñas porque no es seguro.

Anotación por Juan J. Martínez, clasificada en: perl, security, scripting.

Hay 3 comentarios

Gravatar

A mi la implementación también me parece de fiar ;)

El problema del MD5 es que hay varios métodos para rebentarlo, y el hecho de usar un salt, te evita el problema del rainbow table, pero siguen habiendo otros ataques a los que es vulnerable, si bien es verdad, que un atacante necesitaría cierto tiempo (no mucho) para rebentarte una única contraseña, tal vez si hay 10000 usuarios, rebentar todas las contraseñas sea inviable (no estoy seguro de que lo sea).

Sabiendo algo del tema, y aunque estoy algo oxidado, en sitios donde se quiera una seguridad extra, usaría SHA-512 (mínimo), usaría un salt, y realizaría 1000 iteraciones sobre la contraseña hasheada. De esta manera, olvidate de ataques diferenciales, rainbow tables, fuerza bruta, y prácticamente todo.

En ambos casos, los usuarios que usaran contraseñas típicas seguirían siendo vulnerable a un ataque de diccionario. Es decir, si te roban las contraseñas de 10000 usuarios estoy seguro que coges el algoritmo de hash (sea el que sea), te pones a hashear un diccionario con cada uno de los salts de cada contraseña, y me juego lo que quieras a que mínimo el 10% de las contraseñas las sacas.

Nada es seguro, todo es cuestión de tiempo y recursos ;D

por Pau Sanchez, en 2010-06-13 19:27:20

Gravatar

Estoy bastante de acuerdo contigo :)

El tema es más bien tener claro cuáles son los mínimos (con coste asumible).

Lo que me lleva a esto:

apache.org incident report for 04/09/2010

Cito: “JIRA and Confluence both use a SHA-512 hash, but without a random salt. We believe the risk to simple passwords based on dictionary words is quite high, and most users should rotate their passwords“.

A mi me parece un buen FAIL por parte de esos proyectos, teniendo en cuenta que es fácil implementar un salt mínimo.

por Juanjo, en 2010-06-13 19:35:16

Gravatar

El caso es que últimamente estoy bastante liado, pero reconozco que siento una gran curiosidad por coger una base de datos de usuarios y hacer un test a ver en 24, 48 y 72 horas, cuantos passwords podrían salir con un simple ataque de diccionario.

En fin… lo dejaré como tarea pendiente para un futuro indefinido ;)

por Pau Sanchez, en 2010-06-15 23:14:01

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 15 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: