Propósito y relevancia
Las tablas arco iris ayudan a descifrar contraseñas difíciles, es decir, aquellas que ni siquiera se pueden encontrar en un diccionario grande. Las contraseñas se almacenaron históricamente como hashes simples en las bases de datos, y eso es contra lo que son efectivas las tablas rainbow: crear una tabla rainbow única (lenta) y ejecutar cualquier cantidad de bases de datos llenas de hashes contra ella (rápida).
En estos días, cada vez más sistemas utilizan algoritmos de almacenamiento de contraseña adecuados, como Bcrypt, Scrypt o Argon2. Consulte: ¿Cómo [guardar] contraseñas de forma segura? Esos algoritmos ya no son "vulnerables" a las tablas de Rainbow: desde cada hash es único, incluso si las contraseñas son iguales, las tablas de arco iris ya no funcionan.
Es por eso que las tablas de arco iris son impopulares hoy. Incluso si no se usa algo moderno como Argon2, los desarrolladores hoy en día generalmente saben que deberían usar al menos una sal. Eso ya es suficiente para hacer una mesa de arco iris inútil.
Cómo funcionan
Creando una tabla
Imagine que creamos una tabla de arco iris con solo dos cadenas, cada una de longitud 5. La tabla de arco iris es para la función de hash ficticia MD48, que genera 48 bits (solo 12 caracteres hexadecimales). Al construir la cadena, vemos esto:
Chain 0: 0=cfcd208495d5 => z=fbade9e36a3f => renjaj820=7668b2810262 => aL=8289e8a805d7 => ieioB=2958b80e4a3a => WLgOSj
Chain 1: 1=c4ca4238a0b9 => ykI4oLkj=140eda4296ac => Dtp=1b59a00b7dbe => W=61e9c06ea9a8 => 6cBuqaha=d4d2e5280034 => 0uUoAD
Comenzamos con 0
porque es la primera cadena (solo necesitamos algo de valor para empezar). Cuando hacemos hash con MD48, se convierte en cfcd208495d5
. Ahora aplicamos una función de "reducción" que básicamente formatea este hash nuevamente en una contraseña, y terminamos con "z". Cuando volvemos a hacer eso, obtenemos fbade9e36a3f
, luego lo reducimos de nuevo y obtenemos renjaj820
. Hay algunos ciclos más y el resultado final es WLgOSj
.
Lo mismo para la segunda cadena. Simplemente comenzamos con otro valor y hacemos lo mismo. Esto termina en 0uUoAD
.
Nuestra tabla completa del arco iris es ahora esto:
WLgOSj => 0
0uUoAD => 1
Eso es todo lo que tienes que almacenar.
Buscando un hash
Digamos que encontramos un hash en línea, 7668b2810262
. ¡Rompámoslo usando nuestra mesa!
Looking for hash '7668b2810262', reduced to 'aL'.
hashed=>reduced 'aL' to ieioB
hashed=>reduced 'ieioB' to WLgOSj
Found a match, 'WLgOSj' is in our rainbow table:
WLgOSj => 0
The chain starts with '0'. Let's walk that chain and look for the hash.
hashed '0' to cfcd208495d5
hashed 'z' to fbade9e36a3f
hashed 'renjaj820' to 7668b2810262
That hash matches! Found the password: renjaj820
Para jugar contigo mismo, los ejemplos anteriores se crearon usando este script de Python: enlace
Propiedades de escalado
En resumen:
- Las búsquedas rápidas significan tablas más grandes, suponiendo que la cobertura se mantenga igual.
- Una mejor cobertura significa búsquedas más lentas o tablas más grandes.
- Las tablas más pequeñas significan búsquedas más lentas o una cobertura peor.
Las siguientes secciones asumen que el tiempo por hash + reducción es 1µs, y no tiene en cuenta las colisiones. Estos son todos los números de estadio de juego, como ejemplos y no valores exactos.
Tiempo de búsqueda
Si una operación de reducción de hash + toma un microsegundo, generar una tabla con un millón de cadenas y 10 000 reducciones por cadena tomaría aproximadamente 3 horas: chain_length * chain_count / reductions_per_second / seconds_per_hour = 10 000 * 1 000 000 / 1 000 000 / 3600 =
2.8 horas.
Hacer una búsqueda en esa tabla toma en promedio 10 milisegundos. Esto se debe a que normalmente tendremos que pasar por chain_length/2
reducciones antes de encontrar qué cadena contiene el hash. Por ejemplo, podríamos tener que hacer 3000 reducciones en un hash antes de encontrar un valor que esté en la tabla. A continuación, tenemos que volver a hacer esa cadena desde el principio hasta que encontremos el valor coincidente. Si solo tuviéramos que hacer 3000 para encontrarlo en nuestra tabla, entonces debemos hacer 7000 reducciones desde el principio para llegar al punto correcto de la cadena. Básicamente, hacemos tantas operaciones cuando buscamos como cuando generamos una sola cadena. Por lo tanto, el tiempo de búsqueda es 10 000 veces por microsegundo, que es de diez milisegundos (o un centisegundo, si lo desea).
Requisitos de almacenamiento
Cuando desee crear una tabla de búsqueda rápida y completa para una función hash, incluso MD5, todavía necesitará cien mil millones de billones de terabytes de almacenamiento. Eso no es de mucha ayuda. Pero, ¿y si queremos cubrir solo las contraseñas en minúscula hasta 10 caracteres?
Si queremos gastar a lo sumo 30 segundos buscando un hash, y suponiendo que necesitamos 1 microsegundo (una millonésima de segundo) por hash + ciclo de reducción, entonces podemos tener una longitud de cadena de: 1 million × 30 =
30 millones . Hay 26 10 (o 10 14 ) posibles contraseñas en minúsculas de 10 caracteres, y por cadena cubrimos 30 millones de valores. Eso nos deja con 4 millones de cadenas. Sabemos que cada cadena solo tiene almacenados un valor inicial y final, y que los valores son de 10 caracteres cada uno. Entonces, 2 × 10 × 4 million =
76 datos de MiB.
Por supuesto, la generación de la tabla mediante la iteración de todas las contraseñas de 10 caracteres lleva mucho tiempo, pero se puede paralelizar y muestra cuán pequeña puede compararse con el espacio de contraseñas que cubre.