www.jmcejudo.com

Programación, seguridad y criptografía

Publicado por Jose Manuel Cejudo Gausi el miércoles 03 de febrero de 2010 (23:18). Categoría: criptografia.

Descargar PasswordHasher.java

El almacenamiento de contraseñas de forma segura es una tarea relativamente simple y sin embargo no siempre realizada correctamente.

Durante las auditorías de seguridad se encuentran con más frecuencia de la que se debería contraseñas almacenadas de forma poco segura. Suponiendo que un atacante pueda tener acceso al almacén de contraseñas veremos como los formatos habituales que se suelen emplear para almacenar contraseñas son inseguros.

El almacén de contraseñas más común es una tabla de usuarios en la base de datos y la técnica de ataque más común que permite acceder a este almacén es SQL Injection.

Formatos habituales de almacenamiento de contraseñas.

Algunas de las formas más comunes en las que se suele almacenar una contraseña son:

  1. En claro, por ejemplo "secreto".
  2. Codificada, por ejemplo en Base64 "c2VjcmV0bw==".
  3. Cifrada utilizando una clave almacenada en el código fuente o un fichero de configuración.
  4. Código hash simple.

Ninguna de estas formas es correcta.

Nota aclaratoria: codificar consiste en aplicar un algoritmo de codificación a un valor de entrada para producir un valor de salida con un formato distinto siendo este proceso reversible aplicando el mismo algoritmo de codificación a la inversa, cifrar consiste en aplicar un algoritmo de cifrado que utiliza una clave secreta para producir una salida totalmente distinta a la entrada siendo este proceso reversible solo si se conoce el algoritmo y clave utilizados.

La opción (1) es evidentemente insegura, la contraseña está en claro por lo se obtiene su valor directamente. La opción (2) también es extremadamente insegura, cualquier codificación estándar es rápidamente identificable y se puede resolver de forma trivial. Incluso utilizar una codificación personalizada también es inseguro ya que con un poco de criptoanálisis suele ser fácilmente resuelta (otro día contaré un caso concreto de esto mismo en el que trabajé).

La opción (3) puede parecer segura pero plantea los siguientes problemas:

  • La clave de cifrado hay que guardarla de forma segura. Si un atacante tiene acceso a ella puede utilizarla para obtener directamente el valor de todas las contraseñas.
  • Si la clave o el algoritmo de cifrado no son robustos un ataque con éxito supone la vulneración de todas las contraseñas.
  • La contraseña debe entenderse como una información absolutamente confidencial que solo debe conocer el usuario propietario de la misma. Aunque no es recomendable, los usuarios tienden a utilizar la misma contraseña en múltiples servicios de empresas distintas. Por estos dos motivos, no debe ser un comportamiento aceptable el que el propietario de una aplicación pueda acceder al valor en claro de las contraseñas de sus usuarios.
  • Desde el punto de vista de implementación, para realizar el proceso de autenticación no es necesario disponer del valor en claro de la contraseña válida del usuario. Por este motivo no tiene justificación técnica el utilizar un mecanismo de cifrado.

Códigos hash inseguros.

La opción (4) es la que suelen utilizar los analistas/programadores que son conscientes de los problemas de las opciones anteriores. Se trata de almacenar el resultado de un algoritmo hash calculado sobre la contraseña. La lógica del proceso de autenticación consiste en calcular el código hash de la contraseña introducida en la interfaz de autenticación y compararlo con el código hash almacenado para el usuario que se está autenticando, si los dos hash son iguales se puede afirmar que la contraseña introducida coincide con la contraseña del usuario.

La forma más común de calcular este hash es utilizar al algoritmo MD5 sobre la contraseña en claro. Por ejemplo el MD5 del texto "secreto" es el siguiente "e201994dca9320fc94336603b1cfc970". Cualquier algoritmo de hash es irreversible, o sea, es imposible obtener el valor original a partir del código hash utilizando algoritmos. Sin embargo, es posible realizar los siguientes ataques:

  • Generación de colisiones.
  • Ataque de fuerza bruta.
  • Ataque de diccionario.
  • Ataque de códigos precalculados (tablas arcoiris).

Generación de colisiones.

Una colisión de hash se produce cuando se encuentran al menos dos valores distintos que producen el mismo código hash. Este tipo de ataque es más posible en algoritmos de hash débiles o considerados inseguros, como es el caso de MD5. Por otro lado, la viabilidad de un ataque de colisión hash no está solo en encontrar una colisión, si no en que el valor de colisión sea semánticamente equivalente al valor original. En el caso que nos ocupa, tendríamos que encontrar una entrada que generase el mismo hash que la contraseña que queremos atacar, esto resulta bastante costoso en recursos de cálculo y por otro lado seguramente tendríamos restricciones en cuanto al tamaño de las contraseñas.

Ataque de fuerza bruta.

Este ataque consiste en calcular códigos hash para todo un conjunto de posibles valores de entrada (por ejemplo todas las combinaciones de letras minúsculas y dígitos hasta 8 caracteres de longitud) e ir comparando el hash obtenido para cada combinación con el hash atacado. Cuando se encuentre una igualdad hemos encontrado el valor original, en este caso la contraseña. Este ataque está limitado por la cantidad de recursos de cálculo de la se disponga y por la cantidad de combinaciones a probar. Sin embargo, la práctica habitual de los usuarios de utilizar contraseñas simples, de poca longitud y con poca variación de caracteres, hace muy factible este tipo de ataque. Existen muchas utilidades para ataques de fuerza bruta, aunque quizás la más famosa es John the Ripper.

Ataque de diccionario.

Un ataque de diccionario es como un ataque de fuerza bruta pero utilizando una lista de "palabras" predefinida en lugar de generar combinaciones. Estas listas de palabras se denominan diccionarios y es fácil encontrar en la web diccionarios de muchos miles de palabras. Otra práctica habitual de los usuarios de utilizar contraseñas basadas en palabras existentes como "secreto", "privado" o "tesoro" facilita este tipo de ataques. Las utilidades como John the Ripper suelen ofrecer versiones de este ataque que consisten en probar variaciones de las palabras del diccionario, por ejemplo: cambiar mayúsculas por minúsculas o sustituir vocales por dígitos (o = 0, i = 1, e = 3, ...). Por esta razón, variaciones de la palabra "secreto" como las siguientes suelen resolverse rápidamente: SeCreTo, sEcrEt0, s3cr3t0, S3cR3t0, etc...

Ataque de códigos precalculados (tablas arcoiris).

Como hemos visto, el problema principal de un ataque de fuerza bruta son los recursos de cálculo y el tiempo necesarios para el mismo. Una solución es disponer de una tabla con los códigos hash ya calculados para una serie de combinaciones concreta. Esto permite buscar directamente un código hash en esta tabla, de forma que si el valor original del hash a atacar está dentro de las combinaciones de la tabla hash precalculada podremos resolverlo de forma inmediata. Estas tablas se denominan tablas arcoiris y existen varias utilidades para construirlas e incluso podemos utilizar alguna web que nos permite consultar sus tablas:

Por los motivos que hemos visto, ninguna de las opciones comentadas hasta el momento es válida para almacenar contraseñas. Se podría pensar que una opción es obligar a los usuarios a utilizar contraseñas complejas, pero todos sabemos que esto es imposible. El usuario típico tiene una tendencia natural a utilizar contraseñas que de una forma u otra son fácilmente adivinables y si le forzamos a usar una combinación difícil de recordar lo normal es que la contraseña acabe en un post-it pegado al monitor...

Códigos hash aliñados y seguros.

¿Cuál es entonces la forma correcta de generar códigos hash para contraseñas? Aliña tus hash con un poco de sal.

La idea consiste en añadir al valor en claro antes de calcular el hash un valor aleatorio de suficiente longitud (denominado salt). Por ejemplo, en lugar de calcular el hash de la contraseña "secreto" lo haremos de "k39Dm%295/2Dan)lp10m.(ca$0a1-secreto".

De esta simple forma hemos eliminado en la práctica la posibilidad de realizar un ataque de tabla arcoiris (porque no es normal que exista una tabla arcoiris del suficiente tamaño para contener este valor), un ataque de diccionario (porque tampoco es normal que este valor exista en un diccionario de palabras) y un ataque de fuerza bruta (porque la capacidad de cálculo y el tiempo necesario para encontrar el hash de este valor no está al alcance del común de los mortales).

La sal además no tiene por qué ser secreta, porque su principal función es hacer más difíciles los ataques. Sin embargo, para hacer las cosas bien, cada contraseña debería tener una sal distinta y propia (en la tabla de la base de datos tendríamos una columna para el hash de la contraseña y otra columna para la sal utilizada). Esta sal tendríamos que utilizarla en el proceso de autenticación para añadirla a la contraseña introducida por el usuario, generar el hash y compararlo con el de la base de datos.

Para poner las cosas más complicadas a un atacante es recomendable hacer más de una operación de hash sobre la contraseña. Se trata de calcular el hash del hash del hash del hash de la contraseña. Si realizamos 1000 operaciones de hash consecutivas sobre la contraseña estamos multiplicando por 1000 el tiempo medio necesario para un ataque de fuerza bruta, y por otro lado estas mil operaciones no deben tener un impacto notable en el rendimiento de cualquier aplicación que funcione en un hardware medianamente moderno (se suele resolver en menos de un segundo).

Por último es importante utilizar un algoritmo de hash considerado seguro como SHA-2 (NO usar MD5). Otro tipo de algoritmo que se puede utilizar son los HMAC usando la sal como valor de clave.

Resumen.

Para generar un hash de contraseña seguro:

  • Añadir al valor en claro de la contraseña un valor aleatorio único para esa contraseña y de longitud al menos igual al tamaño del hash generado.
  • Utilizar un algoritmo de hash seguro como SHA-512.

Clase Java para generación y validación de hash seguros.

La siguiente clase Java implementa de forma simple una serie de métodos de utilidad para la generación y validación de códigos hash. Utiliza el algoritmo HmacSHA512, una longitud de sal de 512 bits y 1000 iteraciones de hash.

Descargar PasswordHasher.java


Enviar un comentario:
  • Sintaxis HTML: Deshabilitado

Jose Manuel Cejudo Gausi

Consultor y formador en desarrollo de aplicaciones y seguridad, especializado en Java, hacking ético y criptografía.
Perfil profesional en LinkedIn

Copyright © 2010 Jose Manuel Cejudo Gausi. Licencia Creative Commons para todo el contenido salvo se indique lo contrario.