John the Ripper y Funciones de Derivación de Claves (KDFs)

Kali LinuxBeginner
Practicar Ahora

Introducción

En este laboratorio, explorará las Funciones de Derivación de Claves (KDFs), que son algoritmos criptográficos diseñados para dificultar los ataques de fuerza bruta a las contraseñas. A diferencia de las funciones hash simples, las KDF introducen intencionadamente un coste computacional, lo que hace mucho más lento probar muchas suposiciones de contraseñas. Aprenderá sobre KDFs comunes como PBKDF2, bcrypt y scrypt, comprenderá cómo generan hashes de contraseñas únicos y observará cómo una potente herramienta de cracking de contraseñas como John the Ripper interactúa con estas funciones. Finalmente, adquirirá experiencia práctica en la implementación de KDFs para el almacenamiento seguro de contraseñas, reforzando las mejores prácticas en ciberseguridad.

Comprender las KDF (por ejemplo, PBKDF2, bcrypt, scrypt)

En este paso, obtendrá una comprensión fundamental de las Funciones de Derivación de Claves (KDFs). Las KDFs son algoritmos criptográficos que derivan una o más claves secretas a partir de un valor secreto, como una clave maestra o una contraseña. Su propósito principal en el almacenamiento de contraseñas es hacer que los ataques de fuerza bruta y de diccionario sean computacionalmente costosos, incluso si un atacante obtiene las contraseñas hasheadas. Esto se logra ralentizando intencionadamente el proceso de hashing a través de cálculos iterativos y el uso de una "sal" (salt).

Una "sal" (salt) es una cadena de datos aleatoria que es única para cada contraseña. Cuando se hashea una contraseña, la sal se combina con la contraseña antes de hashearla. Esto evita que los atacantes utilicen tablas arcoíris (rainbow tables) precalculadas para descifrar contraseñas y garantiza que dos contraseñas idénticas producirán hashes diferentes si sus sales son distintas.

Exploremos algunas KDFs comunes:

  • PBKDF2 (Password-Based Key Derivation Function 2): Esta función aplica una función pseudorandom (como HMAC-SHA256) a la contraseña de entrada junto con una sal y repite el proceso muchas veces (iteraciones) para aumentar el coste computacional.
  • bcrypt: Basado en el cifrado Blowfish, bcrypt está diseñado para ser adaptable, lo que significa que su coste computacional puede aumentarse con el tiempo para mantenerse al día con el aumento de la velocidad de los procesadores. Es conocido por su resistencia a los ataques basados en GPU.
  • scrypt: Esta KDF fue diseñada específicamente para ser resistente a ataques de hardware (ASICs y FPGAs) al requerir cantidades significativas de memoria, además de potencia computacional.

Para empezar, asegúrese de que John the Ripper esté instalado, ya que lo utilizaremos para demostrar las KDFs más adelante.

john --version

Debería ver una salida similar a esta, indicando que John the Ripper está instalado:

John the Ripper password cracker, version 1.9.0-jumbo-1+bleeding-e7022e5 64-bit
Copyright (c) 1996-2020 by Solar Designer
...

A continuación, creemos un script simple de Python para demostrar cómo se puede usar PBKDF2 para hashear una contraseña.

Cree un archivo llamado kdf_demo.py:

nano kdf_demo.py

Agregue el siguiente código Python al archivo:

import hashlib
import os

def pbkdf2_hash(password, salt=None, iterations=100000):
    if salt is None:
        salt = os.urandom(16) ## Generate a random 16-byte salt

    ## PBKDF2 with HMAC-SHA256
    hashed_password = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        iterations
    )
    return salt, hashed_password

password = "mysecretpassword"
salt, hashed = pbkdf2_hash(password)

print(f"Password: {password}")
print(f"Salt (hex): {salt.hex()}")
print(f"Hashed Password (hex): {hashed.hex()}")
print(f"Iterations: 100000")

Guarde el archivo presionando Ctrl+X, luego Y, luego Enter.

Ahora, ejecute el script de Python:

python3 kdf_demo.py

Verá una salida que muestra la contraseña original, la sal generada y el hash PBKDF2. Tenga en cuenta que la sal es una cadena hexadecimal aleatoria, y el hash también es una cadena hexadecimal. Cada vez que ejecute el script, se generará una nueva sal, lo que resultará en un hash diferente para la misma contraseña.

Password: mysecretpassword
Salt (hex): <random_hex_string>
Hashed Password (hex): <random_hex_string>
Iterations: 100000

Esto demuestra el concepto central de las KDFs: combinar una contraseña con una sal única y aplicar muchas iteraciones de una función de hashing para producir un hash computacionalmente costoso.

Identificar Hashes Generados por KDFs

En este paso, aprenderá a identificar el formato de los hashes generados por diferentes KDFs. Si bien la salida bruta de una KDF puede ser una cadena binaria, cuando se almacenan en sistemas, a menudo se codifican (por ejemplo, Base64 o hexadecimal) y se prefijan con identificadores que indican la KDF utilizada, la sal y, a veces, el número de iteraciones o el factor de coste. Este formato estandarizado permite que herramientas como John the Ripper las reconozcan y procesen correctamente.

Veamos los formatos de hash comunes para PBKDF2, bcrypt y scrypt.

PBKDF2:
Los hashes PBKDF2 a menudo aparecen en un formato que incluye el algoritmo, las iteraciones, la sal y la clave derivada. Por ejemplo, en los archivos /etc/shadow en Linux, los hashes PBKDF2 (específicamente SHA512) pueden verse así:
$6$rounds=5000$<salt>$<hash>
Aquí, $6$ indica SHA-512, rounds= especifica las iteraciones, seguido de la sal y el hash real.

bcrypt:
Los hashes bcrypt son fácilmente reconocibles por su prefijo $2a$, $2b$ o $2y$, seguido del factor de coste (por ejemplo, 10), la sal y el hash.
Ejemplo: $2a$10$<salt><hash>

scrypt:
Los hashes scrypt suelen comenzar con $7$ o $scrypt$, seguidos de parámetros como ln, r, p (coste logarítmico, tamaño del bloque y factor de paralelización), la sal y el hash.
Ejemplo: $7$<ln>$<r>$<p>$<salt><hash>

Para demostrarlo, crearemos un archivo que contenga algunos hashes de KDF de ejemplo. Utilizaremos una herramienta llamada mkpasswd (que forma parte del paquete whois) para generar un hash bcrypt y luego construiremos manualmente un hash PBKDF2 para demostración.

Primero, instale el paquete whois para obtener mkpasswd:

sudo apt install -y whois

Ahora, generemos un hash bcrypt para la contraseña "password123" con un factor de coste de 10.

mkpasswd -m bcrypt -S $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16) -s 10 password123

Este comando genera un hash bcrypt. La opción -S proporciona una sal aleatoria y -s 10 establece el factor de coste en 10. La salida será una cadena de hash bcrypt.

$2a$10$<random_salt_string><hash_string>

Ahora, creemos un archivo llamado kdf_hashes.txt que John the Ripper pueda leer. Incluiremos un hash bcrypt y un hash PBKDF2 creado manualmente.

nano kdf_hashes.txt

Agregue el siguiente contenido al archivo. Reemplace <YOUR_GENERATED_BCRYPT_HASH> con el hash bcrypt real que generó en el paso anterior.

user1:$2a$10$<YOUR_GENERATED_BCRYPT_HASH>
user2:$pbkdf2-sha256$100000$c0ffee$a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef

Nota: El hash PBKDF2 para user2 es un marcador de posición para fines de demostración. No es un hash real de una contraseña conocida, pero sigue el formato que John the Ripper espera para PBKDF2-SHA256. El formato es $pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>.

Guarde el archivo presionando Ctrl+X, luego Y, luego Enter.

Ahora, usemos John the Ripper para identificar los tipos de hash en kdf_hashes.txt:

john --format=raw-md5 --show kdf_hashes.txt

Nota: Usamos --format=raw-md5 como un formato ficticio porque John requiere que se especifique un formato para --show, incluso si solo está identificando. John detectará automáticamente los formatos de KDF reales.

La salida mostrará a John identificando los tipos de hash:

0 password hashes cracked, 2 left

John identifica correctamente que hay dos hashes, y reconocerá sus tipos de KDF cuando intente descifrarlos. Este paso se centra principalmente en reconocer los formatos de hash.

Observar el Soporte de John the Ripper para KDFs

En este paso, observará cómo John the Ripper soporta el descifrado de hashes generados por KDFs. John the Ripper tiene soporte integrado para una amplia gama de tipos de hash, incluyendo varias KDFs como PBKDF2, bcrypt y scrypt. Cuando usted proporciona a John un archivo que contiene estos hashes, detecta automáticamente el tipo de hash y aplica los algoritmos de descifrado apropiados.

Utilizaremos el archivo kdf_hashes.txt creado en el paso anterior. Intentaremos descifrar el hash de user1 (bcrypt) con una lista de palabras simple.

Primero, creemos un pequeño archivo de lista de palabras llamado wordlist.txt que contenga contraseñas comunes, incluyendo "password123".

nano wordlist.txt

Agregue el siguiente contenido a wordlist.txt:

test
123456
password
password123
qwerty

Guarde el archivo presionando Ctrl+X, luego Y, luego Enter.

Ahora, usemos John the Ripper para descifrar los hashes en kdf_hashes.txt usando nuestro wordlist.txt.

john kdf_hashes.txt --wordlist=wordlist.txt

John iniciará el proceso de descifrado. Dado que "password123" está en nuestra lista de palabras y el hash bcrypt para user1 se generó a partir de él, John debería descifrarlo rápidamente.

Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts to test (bcrypt [Blowfish])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
password123      (user1)
1g 0:00:00:00 DONE (2023-10-27 08:30) 100% <random_speed>c/s <random_speed>p/s <random_speed>L/s <random_speed>PC/s user1
Session completed.

Puede ver que John descifró exitosamente user1 e identificó la contraseña como password123. El hash user2 (PBKDF2) no fue descifrado porque su "contraseña" no estaba en nuestra lista de palabras (era un hash de marcador de posición).

Para ver las contraseñas descifradas, puede usar la opción --show:

john --show kdf_hashes.txt

La salida mostrará la contraseña descifrada para user1:

user1:password123

1 password hash cracked, 1 left

Esto demuestra la capacidad de John the Ripper para detectar y descifrar automáticamente hashes KDF, lo que resalta la importancia de usar contraseñas fuertes y únicas incluso con KDFs.

Comprender el Coste Computacional de las KDFs

En este paso, obtendrá una comprensión más profunda del coste computacional asociado con las KDFs y por qué es una característica de seguridad crucial. El objetivo principal de las KDFs es hacer que el descifrado de contraseñas sea computacionalmente costoso, aumentando así el tiempo y los recursos que un atacante necesita para adivinar contraseñas. Este coste se controla mediante parámetros como el número de iteraciones (para PBKDF2) o el factor de coste (para bcrypt y scrypt).

Revisemos nuestro script kdf_demo.py y modifiquémoslo para incluir el hashing bcrypt y scrypt, y observemos el tiempo empleado para cada uno.

Primero, instale la biblioteca passlib, que proporciona implementaciones para varias KDFs:

pip install passlib

Ahora, modifique kdf_demo.py para incluir el hashing bcrypt y scrypt.

nano kdf_demo.py

Reemplace el contenido existente con el siguiente código:

import time
from passlib.hash import pbkdf2_sha256, bcrypt, scrypt

password = "mysecretpassword"

print("--- PBKDF2-SHA256 ---")
start_time = time.time()
## Default iterations for pbkdf2_sha256 in passlib is 29000
pbkdf2_hash = pbkdf2_sha256.hash(password)
end_time = time.time()
print(f"Hash: {pbkdf2_hash}")
print(f"Time taken: {end_time - start_time:.4f} seconds")
print(f"Iterations: {pbkdf2_sha256.identify(pbkdf2_hash).get('rounds')}")
print("-" * 20)

print("--- bcrypt ---")
start_time = time.time()
## Default rounds for bcrypt in passlib is 12
bcrypt_hash = bcrypt.hash(password)
end_time = time.time()
print(f"Hash: {bcrypt_hash}")
print(f"Time taken: {end_time - start_time:.4f} seconds")
print(f"Cost factor (rounds): {bcrypt.identify(bcrypt_hash).get('rounds')}")
print("-" * 20)

print("--- scrypt ---")
start_time = time.time()
## Default parameters for scrypt in passlib are N=2^14, r=8, p=1
scrypt_hash = scrypt.hash(password)
end_time = time.time()
print(f"Hash: {scrypt_hash}")
print(f"Time taken: {end_time - start_time:.4f} seconds")
print(f"N (CPU/Memory cost): {scrypt.identify(scrypt_hash).get('N')}")
print(f"r (Block size): {scrypt.identify(scrypt_hash).get('r')}")
print(f"p (Parallelization): {scrypt.identify(scrypt_hash).get('p')}")
print("-" * 20)

Guarde el archivo presionando Ctrl+X, luego Y, luego Enter.

Ahora, ejecute el script de Python actualizado:

python3 kdf_demo.py

Observe la salida. Verá el hash generado por cada KDF y el tiempo que tardó en generarlo.

--- PBKDF2-SHA256 ---
Hash: $pbkdf2-sha256$<iterations>$<salt>$<hash>
Time taken: <time> seconds
Iterations: <iterations_value>
--------------------
--- bcrypt ---
Hash: $2a$<cost_factor>$<salt_and_hash>
Time taken: <time> seconds
Cost factor (rounds): <cost_factor_value>
--------------------
--- scrypt ---
Hash: $scrypt$<N>$<r>$<p>$<salt_and_hash>
Time taken: <time> seconds
N (CPU/Memory cost): <N_value>
r (Block size): <r_value>
p (Parallelization): <p_value>
--------------------

Notará que incluso para un solo hash, lleva una cantidad medible de tiempo (por ejemplo, milisegundos). Este es el coste computacional intencional. Si un atacante intenta descifrar una contraseña adivinando millones o miles de millones de contraseñas, este pequeño retraso por hash se suma significativamente, haciendo que los ataques de fuerza bruta sean poco prácticos.

Por ejemplo, si una KDF tarda 0.1 segundos en hashear una contraseña, un atacante que intente 1000 adivinanzas por segundo solo podría probar 10 contraseñas por segundo. Esto ralentiza drásticamente el proceso de descifrado en comparación con algoritmos de hashing simples y rápidos.

Los parámetros (iteraciones, factor de coste, N, r, p) se pueden ajustar para aumentar o disminuir este coste computacional. A medida que la potencia de cálculo aumenta con el tiempo, estos parámetros deben aumentarse para mantener el mismo nivel de seguridad.

Implementar KDFs para Almacenamiento Seguro de Contraseñas

En este paso, aprenderá cómo implementar KDFs para el almacenamiento seguro de contraseñas en un escenario práctico. El objetivo es almacenar hashes de contraseñas, no las contraseñas en sí, y utilizar KDFs para hacer que estos hashes sean resistentes al descifrado. Esto implica generar una sal (salt) única para cada contraseña, hashear la contraseña con la KDF y sal elegidas, y almacenar el hash resultante (que incluye la sal y los parámetros de la KDF) en una base de datos o archivo.

Continuaremos utilizando Python y la biblioteca passlib para simular un sistema simple de registro e inicio de sesión de usuarios.

Primero, asegúrese de estar en el directorio ~/project.

cd ~/project

Ahora, cree un nuevo script de Python llamado secure_auth.py:

nano secure_auth.py

Agregue el siguiente código Python al archivo. Este script le permitirá registrar un nuevo usuario con una contraseña (que se hasheará usando bcrypt) y luego verificar un intento de inicio de sesión.

from passlib.hash import bcrypt

## Simular una base de datos de usuarios
## En una aplicación real, esto sería una base de datos (por ejemplo, SQLite, PostgreSQL)
user_db = {}

def register_user(username, password):
    """Hashea la contraseña usando bcrypt y la almacena."""
    if username in user_db:
        print(f"Error: El usuario '{username}' ya existe.")
        return False

    ## Hashea la contraseña usando bcrypt. Passlib maneja la generación de sal y el factor de coste.
    hashed_password = bcrypt.hash(password)
    user_db[username] = hashed_password
    print(f"Usuario '{username}' registrado exitosamente.")
    print(f"Hash almacenado: {hashed_password}")
    return True

def verify_login(username, password):
    """Verifica una contraseña contra el hash almacenado."""
    if username not in user_db:
        print(f"Fallo de inicio de sesión: Usuario '{username}' no encontrado.")
        return False

    stored_hash = user_db[username]

    ## Verifica la contraseña usando bcrypt.verify().
    ## Esta función extrae automáticamente la sal y el coste del hash.
    if bcrypt.verify(password, stored_hash):
        print(f"Inicio de sesión exitoso para el usuario '{username}'.")
        return True
    else:
        print(f"Fallo de inicio de sesión: Contraseña incorrecta para el usuario '{username}'.")
        return False

## --- Demostración ---
print("--- Registrando Usuarios ---")
register_user("alice", "securepassword123")
register_user("bob", "anothersecret")
register_user("alice", "duplicateuser") ## Intento de registrar usuario existente

print("\n--- Intentando Inicios de Sesión ---")
verify_login("alice", "securepassword123") ## Contraseña correcta
verify_login("bob", "wrongpassword")      ## Contraseña incorrecta
verify_login("charlie", "anypassword")    ## Usuario inexistente
verify_login("bob", "anothersecret")     ## Contraseña correcta

Guarde el archivo presionando Ctrl+X, luego Y, luego Enter.

Ahora, ejecute el script secure_auth.py:

python3 secure_auth.py

Observe la salida. Verá el proceso de registro, incluyendo los hashes bcrypt generados, y los resultados de los intentos de inicio de sesión.

--- Registering Users ---
User 'alice' registered successfully.
Stored hash: $2a$<cost_factor>$<salt_and_hash>
User 'bob' registered successfully.
Stored hash: $2a$<cost_factor>$<salt_and_hash>
Error: User 'alice' already exists.

--- Attempting Logins ---
Login successful for user 'alice'.
Login failed: Incorrect password for user 'bob'.
Login failed: User 'charlie' not found.
Login successful for user 'bob'.

Este script demuestra los principios fundamentales del almacenamiento seguro de contraseñas utilizando KDFs:

  1. Hashing en el Registro: Cuando un usuario se registra, su contraseña en texto plano nunca se almacena. En su lugar, se hashea inmediatamente utilizando una KDF (bcrypt en este caso), y el hash resultante se almacena. La función bcrypt.hash() se encarga automáticamente de la generación de la sal y aplica el factor de coste por defecto.
  2. Verificación en el Inicio de Sesión: Cuando un usuario intenta iniciar sesión, su contraseña proporcionada se hashea nuevamente utilizando la misma KDF y parámetros (sal y factor de coste) extraídos del hash almacenado. El hash recién generado se compara luego con el hash almacenado. Si coinciden, el inicio de sesión es exitoso. La función bcrypt.verify() simplifica este proceso.

Al utilizar KDFs, incluso si un atacante obtiene acceso a su user_db (o a una base de datos real), solo tendrá acceso a los hashes computacionalmente costosos, lo que hará que la recuperación de las contraseñas originales sea significativamente más difícil y lenta.

Resumen

En este laboratorio, adquirió una comprensión integral de las Funciones de Derivación de Claves (KDFs) y su papel fundamental en el almacenamiento seguro de contraseñas. Aprendió sobre KDFs populares como PBKDF2, bcrypt y scrypt, reconociendo sus formatos de hash únicos y los parámetros que controlan su coste computacional. Observó cómo John the Ripper, una potente herramienta de descifrado de contraseñas, soporta estas KDFs, destacando la importancia de contraseñas robustas incluso cuando se utilizan KDFs. Finalmente, implementó un sistema básico de almacenamiento seguro de contraseñas utilizando Python y la biblioteca passlib, demostrando la aplicación práctica de las KDFs para hashear y verificar contraseñas. Este laboratorio reforzó el principio de que las KDFs son esenciales para hacer que los ataques de fuerza bruta y de diccionario sean computacionalmente inviables, mejorando significativamente la seguridad de las credenciales de los usuarios.