Share on Google+Share on FacebookTweet about this on Twitter

ATENCION: Los ejemplos que se van a mostrar y “tutoriales” tan solo tienen carácter educativo. En ningún aspecto comparto filosofías de invasión a la intimidad, ataques contra un sistema informático o cuestiones similares. En la medida que sea posible siempre se usarán ejemplos y formas que puedan ser usados por cualquier persona, de forma que pueda verificar los contenidos escritos. No obstante, por motivos más que obvios, materiales como contraseñas, nombres de usuarios o de hosts, serán omitidos o modificado en las capturas de pantallas realizadas (o las lineas escritas). Es decir, los ejemplos serán completamente reales, los datos mostrados a vosotros necesarios para poder pertrechar estos ejemplos no siempre lo serán (Sí lo serán los resultados). Para que esto conste de forma clara, todo material sensible modificado o falso estará resaltado en ROJO. Por motivos de seguridad, todo el material que sea expuesto aquí (exceptuando software propietario o libre, citaciones expresas o código de terceros) tanto texto, imágenes y código son propiedad del autor y está completamente prohibido su reproducción completa o parcial en otros lugares, espero que se comprenda.

 


Hash

A diferencia del Spoofing, si hablamos de encriptación o autentificación se debe de establecer un orden sobre lo que vamos a ir viendo. Esto se debe a que un elemento suele requerir de otro, y este otro de otro… si no se explica adecuadamente cada uno de los elementos, será imposible comprender los que dependan den estos. Y el primero de estos elementos es el hash.

Un hash podemos definirlo como el resultado de una función matemática aplicada a una entrada arbitraria de datos, de forma que el resultado es (idealmente) asociado únicamente a la entrada dada y siempre obteniendo un resultado de longitud finita y concreta para el mismo hash. Es decir, idealmente para los hash criptográficos sería imposible volver a obtener el mismo resultado con otros datos diferentes. La idea es poder convertir la cantidad de datos que sea en un “resultado” de longitud fija (fijada por el propio hash). Veamos un ejemplo muy sencillo de esto. Imaginar uan función hash que realiza lo siguiente sobre números enteros:

Hash = numero1 + numero2 + numero3…. MODULO 100

En dicho ejemplo el hash se calcularía sumando cada numero de entrada dado y se le realizaría la operación Módulo 100. La operación módulo devuelve simplemente el resto de la división, y dado que el divisor es 100, el resto será siempre un número entre 0 y 99:


n1 = 500 n2 = 100 -> Hash = (500+100) MOD 100 = 600 MOD 100 = 0 Hash = 0 (600/100 = 6 y resto 0)

n1 = 1250 n2 = 25 n3 = 5460 Hash = (1250+25+5460) MOD 100 = 6735 MOD 100 = 35 Hash = 35 (6735/100 = 67 y resto 35)

Evidentemente esta función hash sería un tanto absurda desde un punto de vista criptográfico, dado que sería relativamente muy facil obtener el mismo resultado con dos entradas de datos diferentes. Pero sirve para dejar ver más o menos de lo que estamos hablando. En este caso tan solo existen 100 posible resultados, pero se puede observar que si modificásemos cualquier número este podría repercutir en un resultado completamente diferente. Las funciones hash hacen más o menos esto, aunque no con una precisión de 100 posibles valores y de una forma mucho más eficiente, pero la idea es la misma. Pero no solo es útil pensar en criptografía, una función Hash puede tener un valor muy importante simplemente en la detección de errores por ejemplo.

¿Para que sirve esto? Tiene una gran utilidad en muchísimos campos. Podemos decir que existen tres tipos de Hash: Hash checksums, Hash CRC y Hash criptográficos.


CheckSum

Sería el ejemplo más básico de Hash. El concepto apareció de la necesidad de verificar de algún modo la integridad de la transferencia de los datos. Es decir, si estos se habían transmitido de forma alguna de forma errónea. ¿Pero como podemos verificar esto? Podemos tratar los datos de entrada de tal forma que nos de un resultado, de modo que si el resultado es diferente en el destino, los datos son erroneos. Generalmente un CheckSum es una operación matemática basada en sumas. El ejemplo expuesto anteriormente sería un posible ejemplo de checksum. El ejemplo de checksum más simple es el bit de paridad. Imaginar que sea cual sea el bloque de datos de entrada, se le computa la paridad al dato, y esta se le añade al dato final. El checksums en sí mismo es tan solo un bit, un cero o un uno que corresponde a la paridad del dato inicial. Cuando los datos son recibidos por el destino, el destino calcula de nuevo la paridad del bloque y la compara con la paridad recibida. Si coincide los datos son válidos. Evidentemente este checksum tan solo previene contra 1 posible cambio de valor en uno de los bits transmitidos. Vamos a verlo con un ejemplo:

Se desea transmitir la cadena “Casa” desde A hasta B. Imaginar que cada carácter es acompañado con un bit de paridad. Un carácter tiene un valor entre 0 y 255 según la tabla ASCII, es decir, un carácter ocupa 1Byte de datos (8 bits). El carácter “C” equivale al código ASCII x43 (43 en hexadecimal), lo que en binario equivale a “01000011”:

C -> 01000011 -> Se aplica Paridad Par por ejemplo (Hay número par de “unos”? si es asi el resultado es cero, de lo contrario es uno) -> 3 Unos, es impar, bit de paridad par = 1

C -> 01000011 + bit de paridad -> Datos transmitidos: 010000111

De este modo, el destino tan solo tiene que tomar los primeros 8 bits y realizar la misma operación. Si el resultado coincide no hay error o no se ha podido detectar. Si no coincide se ha producido un error y los datos no puede tomarse como válidos. En este caso caso, tan solo se podría detectar con un bit un número impar de errores: 1, 3, 5… dado que si se produce un número par de errores el bit de paridad no cambiaría.


Evidentemente existen Checksums más eficientes, aunque todo depende del uso que se le haga. En cambio todos los días tratamos con este tipo de sistemas, y no hay que profundizar siquiera en la informática. En la mayoría de datos personales que puedan ser sensibles, suele existir un carácter o caracteres de control que verifican que los datos introducidos son válidos. Por ejemplo la letra de nuestro DNI no es más que un checksum, que se obtiene aplicando un módulo 23 al número de la división. Al resultado (entre 0 y 22), se le asigna una letra ya espeificada. Por ejemplo, el Cero es la letra T, el tres es la letra A… de tal forma que si damos o introducimos nuestro DNI de forma incorrecta, es posible verificarlo simplemente comprobando la letra proporcionada y ver si coincide con la que debería de ser. A este ejemplo se le suman los carácter de control de los números de cuenta corriente, de otros datos del DNI, número de la seguridad social… es una forma simple y efectiva de detectar con prontitud cualquier error en los datos tomados.

Otro uso increíblemente extendido es en la verificación de datos en sí mismos, no solo en la transferencia de estos. Así por ejemplo, si se quiere disponer de algún tipo de archivo que pueda tener datos relativamente sensibles como bios, firmwares, datos personales… lo normal es ir aplicando checksums a determinadas partes del archivo incrustando este mismo en las diferentes partes del archivo. De este modo, el software a medida que va procesando el archivo puede ir verificando cada bloque para asegurarse de que el archivo es confiable. Pensar en una bios que se quiere actualizar y por cualquier motivo existe un error en uno solo de sus bits. Es suficiente para que el PC no arranque. Para evitar esto, se distribuye por toda la bios checksums que van verificando bloques menores,

Su uso no obstante se ha ido reduciendo con la aparición de los Hash CRC, los cuales suelen realizar operaciones similares pero de una forma mucho más efectiva. No obstante para pequeños bloques de datos o comprobaciones sencillas suele ser más fácil y barato de implementar Checksums. Entre los Checksums más habituales encontramos Sum8, Sum16, Sum32 o Sum64.


CRC

Se traduce como comprobación de redundancia cíclica. Su objetivo y uso es muy similar al del checksum, de tal modo que no es raro ver lugares en los cuales el nombre de CRC no es más que un tipo de checksum. La necesidad de la verificación de los datos es algo de suma importancia, siendo quizás su máximo exponente la firma digital, de la cual ya hablaremos de ella. Pero no solo la detección de errores es necesaria, a veces es necesaria también la corrección de estos. Aunque un checksum puede usarse para esto, es más normal ver este tipo de correctores como CRC. Aun así, los sistemas de corrección de errores suelen ser costosos en cuanto a rendimiento, precio, implementación… tanto que normalmente no compensa, y es mucho más eficiente simplemente un buen sistema de detección, y si la detección es erronea simplemente se retransmiten de nuevo los datos. Si bien los checksums pueden ser una herramienta muy extendida dentro de los propios archivos, los CRC suelen ser usado de forma mucho más extensa en la transmisión de datos, aplicado normalmente a bloques de datos de un tamaño mayor.

El peor escenario que puede darse en una transmisión de datos o en un error en un archivo, es que este no pueda detectarse, haciendo que los datos sean enviados o procesados como legítimos. Y es esto lo que se debe de evitar a cualquier precio. Por ello, los CRC son usados intensamente en la transmisión de datos sobre internet, telefonía… y por supuesto en la integridad de los datos en un disco duro, CD, pendrive y otros dispositivos. Cuando hablo aquí de integridad no me estoy refiriendo a sistemas de checksums que puedan estar implementados en la misma estructura del archivo, sino sistemas de ingreidad que poseen los propios dispositivos. Gracias a la integración hardware y la simplicidad de como opera un CRC (la mayoría de ellos), la gran mayoría de nuestro hardware implementa funciones CRC en él mismo, sin necesidad de un software. Es decir… todo el proceso es transparente a nosotros.

El funcionamiento de un CRC es simple. A un bloque de entrada se le añade primero los bits correspondiente al CRC, y el nuevo bloque se divide por un polinomio preestablecido. El resto de dicha división binaria será el CRC. Dicho CRC se añade al bloque original y se retransmite. El destino tomará el bloque y lo dividirá por el polinomio generador (que será el mismo que usó el emisor). Si el resto es cero, no hay error de transmisión, o al menos no hay error detectable. La eficiencia radica por lo cual en el polinomio generador usado y evidentemente en el número de bits usados para el CRC. El caso más simple de CRC sería el bit de paridad, que correspondería a un bit de CRC tan solo, y el polinomio generador sería X + 1, el cual se traduciría como “11” como divisor del mensaje de entrada. Un polinomio generador tal como X5+X4+X+3 se traduciría por lo tanto como “110011” y se tendría un CRC de 6 bits.

Aun en los CRC-32, no se puede considerar CRC un hash seguro, no está pensado como resultado único de una posible entrada ni como sistema de ocultación o cifrado de datos, sino como un sistema robusto de detección de errores, y francamente, hace su trabajo a la perfección. Gracias a este tipo de funciones hash, a día de hoy disponemos de medios de comunicación fiables y con una gran tolerancia a fallos. Que no apreciemos este tipo de tecnologías, no implica que las estemos usando constantemente. Para hacernos una idea de la eficiencia de los CRC, un CRC-16 tienes el siguiente índice de detección:

  • Detección del 100% de errores simples (errores que afectan tan solo a un bit)
  • Detección del 100% de errores dobles de bits adyacentes (Si dos bits consecutivos son erróneos)
  • Detección del 100% de los errores para datos de hasta 16 bits.
  • Detección del 100% de todos los errores de dos bits que no estén separados uno del otro exactamente a 216-1 bits
  • Para el resto de posibles errores, se establece tan solo una no detección en un fallo de cada 216

Es decir, un cRC-16 sería capaz de detectar aproximadamente el 99.995% de todos los errores. ¿Esto es mucho o es poco? Esto equivale a que cada 20.000 errores, uno no se detecta. Teniendo en cuenta las comunicaciones ultrarápidas de hoy en día, la cantidad de información que es manejada en segundos es simplemente impresionante, por lo cual podemos afirmar que de cuando en cuando efectamente aparecerán errores no detectados. Esto se subsana también gracias a la fiabilidad cada vez mayor de las propias redes, con menos ruido, con mejores equipos…

Los CRC más comunes son CRC8, CRC16, CRC32… aunque si se desea ver una lista de ellos con sus polinomios generadores tan solo hay que acudir a la Wikipedia, por ejemplo.


Criptografía

En realidad son los Hash que nos van a interesar a nosotros. Este tipo de hash se usan ya no solo como detectores de errores (que pueden valer para ello también). Este tipo de Hash, al igual que los que hemos visto, es u procedimiento determinista que devolverá un resultado de una longitud fija. Pero a diferencia de los CRC o checksums en los que su objetivo principal es la detección de errores, la función de un hash criptográfico va mucho más allá:

  • La imposibilidad de poder encontrar una cadena (un bloque de datos…) cuyo resultado sea un hash dado.
  • La imposibilidad de modificación de la cadena inicial, sin modificar el hash.
  • La imposibilidad de encontrar una segunda cadena que verifique un hash de otra cadena.
  • Un hash que sea computacionalmente eficiente y facil de implementar.

Hay que comprender que con imposibilidad no podemos asegurar que sea imposible, sino que el coste computacional para ello sería tan ingente que de ninguna forma sería factible. Esto evidentemente no es más que la teoría, en la práctica todo no es tan simple.

A diferencia de los CRC o los checksums que pueden comprenderse sin muchas nociones de matemáticas y parten de unos conceptos simples, los hash criptográficos son bastante más complicado de comprender (las matemáticas escondidas detrás de ellos). Cada algoritmo hash tiene sus propios fundamentos, basados en premisas diferentes, siempre intentando cumplir cada una de las premisas dadas. No obstante podemos citar los Hash criptográficos más usados a día de hoy, como pueden ser: MD4, MD5, RIPEMD, SHA-1, SHA-256, SHA-512. Por supuesto existen muchos otros, aunque menos usados. Posiblemente a muchos algunas de esos nombres les sea conocido.

Al cumplir las premisas citadas, los Hash criptográficos suelen ser usados de forma intensiva en los siguientes campos:

  • Comprobaciones de archivos
  • Firmas digitales
  • Tablas Hash
  • Integridad de un mensaje/contenido


Dado que un Hash puede ser usado para detectar errores en el envío y/o recepción de los datos, un Hash criptográfico puede ejercer función de Comprobador de archivos. Mientras que CRC o checksum suelen aplicarse normalmente a porciones de código, tramas en las comunicaciones, pequeños detectores… este tipo de hash en realidad no se diseñan como correctores de errores o para que el contenido sea reenviado si no es correcto. Este tipo de Hash se suelen aplicar sobre un archivo completo (o conjunto de ellos). Para estos Hash, no importa lo grande o pequeño que sea el dato a verificar (pensado especialmente para grandes cantidades de datos en comparación con CRC o checksum claro está).

Comprender su función en este caso es simple. Al contenido original se le apluca un Hash criptográfico el cual se adjunta al software/archivo original. Cuando el receptor lo descarga, tiene acceso a él… solo tiene que calcular el mismo el mismo Hash y comprobarlo con el Hash que ha sido descargado desde la fuenta original. La idea es que sl el hash es el mismo, tenemos la certeza de que el archivo no se ha corrompido por el envío y la recepción ha sido satisfactoria. Por un principio similar, se puede verificar la integridad y veracidad de dicho archivo, que no ha sido modificado por nadie, que es legítimo. Podemos afirmar esto dada las propiedades ya vistas de este tipo de Hash: La imposibilidad de poder encontrar o crear otro archivo que pudiese coincidir con el hash del archivo original, y por otro lado no sería posible modificar el contenido del archivo sin alterar el hash que se calcularía en el destino.

Ver esto es muy fácil con algunos ejemplos. Tan solo tenemos que buscar un software que sea distribuido por razones de seguridad con su hash. En este caso vamos a usar por ejemplo la imagen de Windows 7 x64 Ultimate ENG. Imaginad que no te quieres molestar en ir a la tienda y encuentras un vendedor supuestamente autorizado que te permite descargar una copia de la imagen de Windows 7 (la misma que he expuesto ahi). Pero claro… quieres asegurarte de que la imagen sea legítima, y no sea una imagen modificada a la que se le haya puesto una activación o algún crack para poder instalarla. Solución? Conocer el Hash de la imagen legítima. Me pongo en contacto con Microsoft o investigo un poco para conocer el Hash de la imagen original y lo que obtengo es lo siguiente:

Windows 7 Ultimate x64 ENG: 7600.16385.090713-1255_x64fre_client_en-us_Retail_Ultimate-GRMCULXFRER_EN_DVD.ISO
MD5: F43D22E4FB07BF617D573ACD8785C028
SHA-1: 326327CC2FF9F05379F5058C41BE6BC5E004BAA7

Lo único que se debe de hacer en este caso es verificar si los valores que yo obtengo al calcular el hash son esos o difieren. En teoría con el cálculo de uno de ellos es suficiente. Para hacer esto se puede usar por ejemplo la utilidad md5sum sha1sum:

E:\Windows 7 Ultimate Final (EN-DE-JP-AR)>md5sum 7600.16385.090713-1255_x64fre_client_en-us_Retail_Ultimate-GRMCULXFRER_EN_DVD.iso
f43d22e4fb07bf617d573acd8785c028 *7600.16385.090713-1255_x64fre_client_en-us_Retail_Ultimate-GRMCULXFRER_EN_DVD.iso

E:\Windows 7 Ultimate Final (EN-DE-JP-AR)>sha1sum 7600.16385.090713-1255_x64fre_client_en-us_Retail_Ultimate-GRMCULXFRER_EN_DVD.iso
326327cc2ff9f05379f5058c41be6bc5e004baa7 *7600.16385.090713-1255_x64fre_client_en-us_Retail_Ultimate-GRMCULXFRER_EN_DVD.iso

Si dicha imagen hubiese sufrido cualquier tipo de modificación el resultado sería muy diferente. Por ejemplo, si a la imagen le modifico simplemente el primer bit (que es un “cero”, y lo establezco a “uno” con un editor hexadecimal) y le recalculo el hash MD5, esto es lo que obtengo:

4420bc0022a2ca8a361111b7a4d24ea7

Es decir, modificando tan solo un bit, el hash es completamente diferente. Por los mismos principios sería en la práctica imposible modificar aleatoriamente (o a conciencia) los bits de forma que pudiese obtener el mismo hash. Y he dicho en la práctica por una razón concreta que ahora veremos.

Vamos a suponer el caso concreto del Hash MD5. El Hash MD5 es un hash de 128 bits, lo que significa que cualquier contenido al que se le aplique este hash, se obtendrá una salida de 128 bits, una cadena de 32 caracteres hexadecimales. Es decir, sin entender siquiera de paradojas o estadística, podríamos afirmar que podríamos obtener un máximo de 2128 posibles hash. Esto es un número un tanto ingente, tanto que posiblemente una mente no es capaz de cuantificar, hablamos de 3.4 * 1038 es decir… 34 con 37 ceros a su derecha. Pero aun cuando este número es mentalmente imposible de imaginar, si es posible de imaginar que en el peor de los casos, cada ese número de hash calculados estos se volverán a repetir, lo cual implica evidentemente que sería teóricamente posible encontrar dos archivos exactamente con el mismo hash. Esta afirmación en realidad no rompe los esquemas vistos, dado que no se “rompe” la veracidad al decir que es improbable modificar un archivo para obtener un hash concreto o encontrar ese segundo archivo que verifique dicho hash. Aunque teóricamente esto es posible, aun cuando solo fuese de forma estadística.

Este es por tanto uno de los principales problemas de los hash criptográficos, y a esto se le llama colisión. Al margen de lo bueno o malo que sea el Hash, estadísticamente es posible encontrar dos hash iguales. En este caso concreto visto, aunque es posible teóricamente, en la práctica si el Hash está bien diseñado sería imprácticable. Cuanto tiempo necesitaría un PC en ser capaz de encontrar una colisión? Pues haciendo números muy por encima… 15 * 1010 años en el supuesto de que toda la población mundial tenga un procesador de 4 núcleos trabajando al mismo tiempo en la misma tarea 24 horas al día. Es decir… en el peor de los casos sería virtualmente imposible.

El problema es que esta lógica no es así. Cuando se habla de una colisión en un hash hay que recordar la llamada “Paradoja del cumpleaños”. Cabe señalar de nuevo que es muy diferente encontrar un contenido que verifique un hash concreto a encontrar dos contenidos disferentes que posean un mismo hash. Lo segundo es una colisión. Es cierto que para el primer caso la probabilidad sería la ya citada, pero no para el segundo caso, y aquí aparece lo que puede parecer increible: La paradoja del cumpleaños establece que en una habitación con 23 personas, existe un 50% de probabilidad de que dos personas cumplan años el mismo día, y si fuesen 60 personas la probabilidad sería del 99%. No hay truco, simplemente se busca una coincidencia entre cualquiera de las 23 personas, no una coincidencia concreta dentro de las 22 restantes. Teniendo esto en cuenta, MD5 posee tan solo 232 posibilidades de encontrar una colisión. Es decir, 4294967296 hash calculados de 4294967296 archivos aleatorios, estadísticamente debería de existir alguna colisión, es decir, dos archivos diferentes que poseen el mismo hash. Y es evidente que este número si que es comprensible y relativamente bajo, dado que un PC normal podría generar colisiones de hash MD5 con relativa facilidad, y esto comenzaría a invalidar los puntos en los que se asienta un Hash criptográfico. Es por esto que MD5 ha dejado de considerarse un Hash seguro, y es solo cuestión de tiempo que quede en desuso, a favor de otros Hash más seguros.

En teoría cualquier Hash debería de presentar posibilidad de Colisión, aunque es evidente que si esta probabilidad es computacionalmente imposible, podemos afirmar que no existe colisión (aunque teóricamente exista). Para tener presente esto, pensar que al Hash MD4 es posible encontrarle colisiones con tan solo en 256 hash.

A raiz de las Colisiones, aparecieron las primeras herramientas que han empezado a romper del todo el Hash MD5. A día de hoy existen herramientas capaces de generar dos programas diferentes que satisfagan el mismo Hash MD5, con lo que se rompe la seguridad de MD5 para la verificación de integridad y comprobación de un contenido. Es evidente que esto tiene matices. A día de hoy continua siendo imposible generar un contenido nuevo que satisfaga un hash buscado (lo cual rompería de forma definitiva el Hash). Pero en cambio si es posible producir dos archivos o dos contenidos que satisface el mismo hash. Esto quedó de manifiesto por el doctor Xiaoyun Wang, el cual incluso liberó el código de una aplicación que es capaz de realizar esto (En la cabecera de este artículo se puede encontrar)

SHA-1 es el segundo Hash más usado a día de hoy. A diferencia de MD5 (aunque basado en sus mismos principios) es un hash de 160 bits, al cual se le ha podido establecer un índice de colisiones de 252 en el mejor de los casos. En dicho caso el cálculo de una colisión sería relativamente práctica de llevar a acabo, quizás un año o dos años en poder lograr encontrar dos contenidos que compartan el mismo hash. No obstante se le continúa considerando seguro.

SHA-2 (conocidos como SHA256 y SHA-512) funcionan de forma muy similar a SHA-1, anque en este caso producen salidas de 256 y 512 bits respectivamente. En ambos casos no se conocen colisiones posibles.

Ante todo esto y dado que podemos asumir que tanto MD5 o SHA-1 son algo así como estándares, ya está en marcha el nuevo “concurso” que será seleccionado como sucesor de SHA-1/SHA-2 y que posiblemente será el próximo estandar en Hash dentro de un par de años. Actualmente se ha comenzado la segunda ronda, y a final de este año debería de quedar todo más o menos finalizado. La idea es encontrar un Hash más seguro y que sea muy eficiente su cálculo ,es decir… la velocidad con la que se pueda calcular el hash a un contenido. Se puede acceder a una lista de todos los candidatos de la segunda ronda en la web oficial del NIST (Instituto nacional de estándares y tecnología)

 

El último uso que deberíamos explicar son las Tablas Hash. Antes de entrar en detalle sobre este tipo de prácticas sería más correcto hablar antes de la Sal o Salt (en inglés). Hasta ahora hemos visto funciones de los Hash criptográficos para comprobar la integridad de los archivos, pero ¿que sucede si queremos usar un Hash como una especie de “encriptador” de contenido? Esto podría no tener mucho sentido dado que cualquier persona puede calcular un hash MD5 por ejemplo a cualquier entrada… pero en cambio no es posible partir del hash para obtener el contenido. Esto adquiere mucha más relevancia cuando se usan un hash para proteger detrás de él un contenido pequeño como un nombre de usuario o una contraseña, y es aquí donde aparece el término y la idea de Salt. Salt es un apéndice que se añade a una cadena de entrada para generar un Hash no intuituvo

Dentro de la web, las cookies y otros contenidos que puedan ser sensibles de cara al exterior como nombres y contraseñas puede ser sometido a un hash criptográfico para esconder su “significado” original. Esto nos dará como resultado una salida “única”, con lo que se podría usar dicha salida como contraseña y nombre del usuario de cara a un servidor, en vez del texto plano. Esto incrementa de forma exponencial la seguridad de cualquier base de datos o sistema de control de acceso. Pero como hemos dicho la utilidad de esto podría ser relativa. Vamos a ver esto con 3 ejemplos que ilustrarán la eficacia o no eficacia de un Hash para estos procesos, así como la implicación de Salt:

Imaginemos que hemos robado un archivo que guarda las credenciales de acceso a una importante base de datos. Imaginemos que dichas credenciales pueden ser almacenadas en dicho archivo como texto plano, MD5 y MD5+Salt. Si abrimos ese documento encontraríamos esto para cada una de las opciones:

1º. Nombre: Theliel Contraseña: perico

2º. Nombre: 5A04B2D961488CDA31CD065F259783BE Contraseña: DFE483413E24A5B1506389D36EBFD05C

3º. Nombre: 217B11413677EE9D4806967515D66607 Contraseña: 8E50E5A474DDAF3BC370F87DD97EC7F0

En el primer caso, es evidente que si está configurado como texto plano, las credenciales serán tomadas de forma directa y rápidamente podremos acceder a la base de datos.

En el segundo caso no obstante n oparece que sea posible descifrar absolutamente nada… ¿pero que pasaría si hacemos uso de la inteligencia? Podemos intuir que es un hash, y si buscamos información del sistema podemos incluso conocer que se trata de un Hash MD5. No podemos revertir el MD5 (a priori), pero en cambio si podemos presuponer el nombre de usuario y ver si hay una coincidencia con el hash que tenemos. Dado que el atacante es listo, comenzaría por cotejar en un diccionario que ya tiene el hash. Su diccionario no es más que una lista precalculada con quizás millones de posibles nombres de usuarios a los que ya se les ha calculado el hash correspondiente. Si el hash se encuentra en su diccionario, obtendrá de forma automática le nombre de usuario. Esto mismo se puede aplicar a la contraseña. Que usuarios se probarán primero? Admin, admin, theliel, Theliel… y en este caso, el diccionario encontraría que dicho hash corresponde a la palabra “Theliel”. En caso de la contraseña es exactamente igual, si la palabra o frase empleada en la contraseña existe en su diccionario, obtendrá directamente la contraseña buscada. Es por ello que siempre es importante tener una contraseña alfanumérica de una longitud decente.

En el tercer caso la cosa es más complicada. El atacante agotaría todos sus diccionarios y no lograría encontrar el hash deseado. ¿Por qué? Porque lo que no sabe el atacante es que el programa que codificó el hash usó una Salt, un trozo de datos que simplemente añadió al final del usuario y la contraseña. Así si el usuario escribió el nombre de usuario: “Theliel”, el servidor jamás lo tomó como tal, sino que automáticamente le añadió el Salt “TATA” (en este caso). Es decir, de cara al servidor cualquier dato introducido es concatenado con “TATA”. Así, el servidor no calcularía el hash de “Theliel” o de “perico” (la contraseña), sino de “ThelielTATA” y “pericoTATA”. dicha modificación es seguro que no aparecerá en su diccionario. La única opción del atacante es conocer la Salt usada por dicho servidor, y crear así un programa que automatice el proceso, recalculando todos los hash de su diccionario con la Salt aplicada y así con suerte obtener algún resultado. Esto lo trataremos mejor cuando se vean las diferentes técnicas para romper la seguridad.

Pero volvamos a las tablas Hash. Hemos explicado que la Salt o la importancia que puede tener un hash para “esconder” unos datos, pero ¿qué es una tabla hash? Dado que el índice de colisiones es relativamente alto, podemos presuponer que no será posible dar la casualidad de tener a dos nombres de usuarios que compartan el mismo hash. Si esto es cierto, para un servidor es mucho más seguro no guardar jamas en sus bases de datos el usuario o la contraseña como tal, solo sus Hash. Al introducir los datos el usuario, son sus hash los que alcanzan el servidor y este simplemente tiene que cotejar dicho hash (el usuario) en su base de datos para comprobar si existe una coincidencia. Si existe tal coincidencia verificar el hash de la contraseña con el hash de contraseña ya almacenado. De este modo nuestros datos de sesión no serían jamás enviados como tales. Pero la utilidad de las tablas de hash radica no en la seguridad (que por supuesto también lo es) sino su eficiencia.

Hemos dicho que el servidor debería de verificar si el hash de nombre de usuario existe en su base de datos. ¿Pero como hace esto? Si nuestra base de datos posee 100 registros, en el peor de los casos la base de datos debería de hacer 100 comprobaciones, para acabar en el último registro que sería el que coincidiese con el hash del usuario introducido. Pero aun, si el usuario introducido no se encontrase en la propia base de datos, esta la habría recorrido entera buscando una coincidencia. Este proceso sería muy costoso para los servidores. Ahora bien, partimos de la premisa que el índice de colisión de un hash MD5 es relativamente alta, 4 mil millones aproximadamente. Podríamos calcularle simplemente el módulo a dicho Hash (en función del número de índices que tengamos en la base de datos), el resultado sería un número de 1 a X, siendo X el número de entradas posibles en nuestra base de datos. Es decir, pongamos que nuestra base de datos tiene 100 registros insertados y tiene una capacidad máxima de 997 (por ser un número primo). Es decir, se aplicaría la operación módulo 997 a cada hash de entrada. Esto convertiría todos los hash de entrada en un número que iría desde el 0 al 996. Este número sí podría ser usado como índice, luego el acceso al registro en la base de datos sería inmediato. Por razones de precisión, usar un número de 128 bits no es aconsejable, lo normal es acotar este número a una resolución de 64bits, tomando por ello los 64 bits primeros del hash o los 64 bits últimos de este. En el ejemplo anterior, al Hash “Theliel” se le aplicaría módulo 997, y el resultado sería: 5A04B2D961488CDA31CD065F259783BE -> 5A04B2D961488CDA MOD 997 = 763. Es decir, que el nombre de usuario Theliel sería convertido al índice 763 en la base de datos. De este modo, al introducir “Theliel” en el navegador, se calcularía el Hash, en el servidor se truncaría y daría como resultado un índice. Con este índice el acceso a la base de datos sería directo, “Acceso a elemento 763”. Asociado a dicho índice se podría encontrar por ejemplo el hash de la contraseña y se procedería a realizar una simple comparación, si los dos hash coinciden se obtiene el acceso.

Esto evidentemente multiplica exponencialmente la posibilidad de una colisión, dado que el espacio disponible ahora es de tan solo 997 elementos. Como hemos dicho la posibilidad de colisión dependerá en gran medida de la ocupación del espacio disponible. Por la paradoja del cumpleaños no obstante, se daría el caso que con unos 35-40 elementos introducidos la probabilidad de una colisión sería de un 50%!!. Para evitar esto se acude a tablas muco mucho mayores en relación al indice esperado de ocupación que se tendrá. Es decir, se sacrifica espacio en post de velocidad. En la Wikipedia aparece un ejemplo parejo, en el que se dice que con 2500 elementos introducidos en una tabla de un millón de elementos, la probabilidad de colisión ascendería al 95%. Que hacer en caso de colisión? Primero evitarlas, ya sean con grandes tablas o con crecimiento dinámico de estas. Por otro lado asumir que es posible que exista una colisión, y diseñar el sistema de forma que ante una colisión sea necesaria una segunda búsqueda en los registros afectados para determinar el destino final.