John the Ripper et les Fonctions de Dérivation de Clés (KDFs)

Kali LinuxBeginner
Pratiquer maintenant

Introduction

Dans ce laboratoire, vous explorerez les Fonctions de Dérivation de Clés (KDFs), qui sont des algorithmes cryptographiques conçus pour rendre les attaques par force brute sur les mots de passe plus difficiles. Contrairement aux fonctions de hachage simples, les KDFs introduisent intentionnellement un coût computationnel, rendant beaucoup plus lent le test de nombreuses tentatives de mots de passe. Vous découvrirez les KDFs courants tels que PBKDF2, bcrypt et scrypt, comprendrez comment ils génèrent des hachages de mots de passe uniques, et observerez comment un outil puissant de cassage de mots de passe comme John the Ripper interagit avec ces fonctions. Enfin, vous acquerrez une expérience pratique dans l'implémentation des KDFs pour le stockage sécurisé des mots de passe, renforçant ainsi les meilleures pratiques en matière de cybersécurité.

Comprendre les KDFs (par ex. PBKDF2, bcrypt, scrypt)

Dans cette étape, vous acquerrez une compréhension fondamentale des Fonctions de Dérivation de Clés (KDFs). Les KDFs sont des algorithmes cryptographiques qui dérivent une ou plusieurs clés secrètes à partir d'une valeur secrète telle qu'une clé maîtresse ou un mot de passe. Leur objectif principal dans le stockage des mots de passe est de rendre les attaques par force brute et par dictionnaire coûteuses en termes de calcul, même si un attaquant obtient les mots de passe hachés. Ceci est réalisé en ralentissant intentionnellement le processus de hachage par des calculs itératifs et l'utilisation d'un "salt".

Un "salt" est une chaîne de données aléatoire unique pour chaque mot de passe. Lorsqu'un mot de passe est haché, le salt est combiné avec le mot de passe avant le hachage. Cela empêche les attaquants d'utiliser des tables arc-en-ciel pré-calculées pour casser les mots de passe et garantit que deux mots de passe identiques produiront des hachages différents si leurs salts sont différents.

Explorons quelques KDFs courants :

  • PBKDF2 (Password-Based Key Derivation Function 2) : Cette fonction applique une fonction pseudo-aléatoire (comme HMAC-SHA256) au mot de passe d'entrée ainsi qu'à un salt et répète le processus de nombreuses fois (itérations) pour augmenter le coût computationnel.
  • bcrypt : Basé sur le chiffrement Blowfish, bcrypt est conçu pour être adaptatif, ce qui signifie que son coût computationnel peut être augmenté au fil du temps pour suivre l'augmentation de la vitesse des processeurs. Il est connu pour sa résistance aux attaques basées sur GPU.
  • scrypt : Ce KDF a été spécifiquement conçu pour résister aux attaques matérielles (ASICs et FPGAs) en nécessitant une quantité significative de mémoire, en plus de la puissance de calcul.

Pour commencer, assurons-nous que John the Ripper est installé, car nous l'utiliserons pour démontrer les KDFs plus tard.

john --version

Vous devriez voir une sortie similaire à celle-ci, indiquant que John the Ripper est installé :

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

Ensuite, créons un script Python simple pour démontrer comment PBKDF2 peut être utilisé pour hacher un mot de passe.

Créez un fichier nommé kdf_demo.py :

nano kdf_demo.py

Ajoutez le code Python suivant au fichier :

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")

Enregistrez le fichier en appuyant sur Ctrl+X, puis Y, puis Entrée.

Maintenant, exécutez le script Python :

python3 kdf_demo.py

Vous verrez une sortie affichant le mot de passe d'origine, le salt généré et le hachage PBKDF2. Notez que le salt est une chaîne hexadécimale aléatoire, et le hachage est également une chaîne hexadécimale. Chaque fois que vous exécutez le script, un nouveau salt sera généré, résultant en un hachage différent pour le même mot de passe.

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

Cela démontre le concept fondamental des KDFs : combiner un mot de passe avec un salt unique et appliquer de nombreuses itérations d'une fonction de hachage pour produire un hachage coûteux en calcul.

Identifier les Hachages Générés par les KDFs

Dans cette étape, vous apprendrez à identifier le format des hachages générés par différents KDFs. Bien que la sortie brute d'un KDF puisse être une chaîne binaire, lorsqu'ils sont stockés dans des systèmes, ils sont souvent encodés (par exemple, Base64 ou hexadécimal) et préfixés par des identifiants qui indiquent le KDF utilisé, le salt, et parfois le nombre d'itérations ou le facteur de coût. Ce format standardisé permet à des outils comme John the Ripper de les reconnaître et de les traiter correctement.

Examinons les formats de hachage courants pour PBKDF2, bcrypt et scrypt.

PBKDF2 :
Les hachages PBKDF2 apparaissent souvent dans un format qui inclut l'algorithme, les itérations, le salt et la clé dérivée. Par exemple, dans les fichiers /etc/shadow sous Linux, les hachages PBKDF2 (spécifiquement SHA512) peuvent ressembler à ceci :
$6$rounds=5000$<salt>$<hash>
Ici, $6$ indique SHA-512, rounds= spécifie les itérations, suivi du salt et du hachage réel.

bcrypt :
Les hachages bcrypt sont facilement reconnaissables par leur préfixe $2a$, $2b$, ou $2y$, suivi du facteur de coût (par exemple, 10), du salt et du hachage.
Exemple : $2a$10$<salt><hash>

scrypt :
Les hachages scrypt commencent généralement par $7$ ou $scrypt$, suivis de paramètres tels que ln, r, p (coût logarithmique, taille du bloc et facteur de parallélisation), du salt et du hachage.
Exemple : $7$<ln>$<r>$<p>$<salt><hash>

Pour démontrer, créons un fichier contenant quelques exemples de hachages KDF. Nous utiliserons un outil appelé mkpasswd (qui fait partie du paquet whois) pour générer un hachage bcrypt, puis nous construirons manuellement un hachage PBKDF2 à titre de démonstration.

Tout d'abord, installez le paquet whois pour obtenir mkpasswd :

sudo apt install -y whois

Maintenant, générons un hachage bcrypt pour le mot de passe "password123" avec un facteur de coût de 10.

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

Cette commande génère un hachage bcrypt. L'option -S fournit un salt aléatoire, et -s 10 définit le facteur de coût à 10. La sortie sera une chaîne de hachage bcrypt.

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

Maintenant, créons un fichier nommé kdf_hashes.txt que John the Ripper pourra lire. Nous inclurons un hachage bcrypt et un hachage PBKDF2 créé manuellement.

nano kdf_hashes.txt

Ajoutez le contenu suivant au fichier. Remplacez <YOUR_GENERATED_BCRYPT_HASH> par le hachage bcrypt réel que vous avez généré à l'étape précédente.

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

Note : Le hachage PBKDF2 pour user2 est un espace réservé à des fins de démonstration. Ce n'est pas un vrai hachage d'un mot de passe connu, mais il suit le format attendu par John the Ripper pour PBKDF2-SHA256. Le format est $pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>.

Enregistrez le fichier en appuyant sur Ctrl+X, puis Y, puis Entrée.

Maintenant, utilisons John the Ripper pour identifier les types de hachage dans kdf_hashes.txt :

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

Note : Nous utilisons --format=raw-md5 comme format factice car John nécessite qu'un format soit spécifié pour --show, même s'il s'agit simplement d'une identification. John détectera automatiquement les formats KDF réels.

La sortie montrera John identifiant les types de hachage :

0 password hashes cracked, 2 left

John identifie correctement qu'il y a deux hachages, et il reconnaîtra leurs types KDF lorsqu'il tentera de les casser. Cette étape se concentre principalement sur la reconnaissance des formats de hachage.

Observer le Support de John the Ripper pour les KDFs

Dans cette étape, vous observerez comment John the Ripper prend en charge le cassage des hachages générés par les KDFs. John the Ripper dispose d'un support intégré pour une large gamme de types de hachage, y compris divers KDFs comme PBKDF2, bcrypt et scrypt. Lorsque vous fournissez à John un fichier contenant ces hachages, il détecte automatiquement le type de hachage et applique les algorithmes de cassage appropriés.

Nous utiliserons le fichier kdf_hashes.txt créé à l'étape précédente. Nous tenterons de casser le hachage de user1 (bcrypt) avec une simple liste de mots.

Tout d'abord, créons un petit fichier de liste de mots nommé wordlist.txt contenant des mots de passe courants, y compris "password123".

nano wordlist.txt

Ajoutez le contenu suivant à wordlist.txt :

test
123456
password
password123
qwerty

Enregistrez le fichier en appuyant sur Ctrl+X, puis Y, puis Entrée.

Maintenant, utilisons John the Ripper pour casser les hachages dans kdf_hashes.txt en utilisant notre wordlist.txt.

john kdf_hashes.txt --wordlist=wordlist.txt

John commencera le processus de cassage. Comme "password123" est dans notre liste de mots et que le hachage bcrypt pour user1 a été généré à partir de celui-ci, John devrait le casser rapidement.

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.

Vous pouvez voir que John a réussi à casser user1 et a identifié le mot de passe comme password123. Le hachage user2 (PBKDF2) n'a pas été cassé car son "mot de passe" n'était pas dans notre liste de mots (c'était un hachage de substitution).

Pour afficher les mots de passe cassés, vous pouvez utiliser l'option --show :

john --show kdf_hashes.txt

La sortie affichera le mot de passe cassé pour user1 :

user1:password123

1 password hash cracked, 1 left

Cela démontre la capacité de John the Ripper à détecter et casser automatiquement les hachages KDF, soulignant l'importance d'utiliser des mots de passe forts et uniques, même avec les KDFs.

Comprendre le Coût Computationnel des KDFs

Dans cette étape, vous acquerrez une compréhension plus approfondie du coût computationnel associé aux KDFs et pourquoi il s'agit d'une caractéristique de sécurité cruciale. L'objectif principal des KDFs est de rendre le cassage de mots de passe coûteux en termes de calcul, augmentant ainsi le temps et les ressources nécessaires à un attaquant pour deviner les mots de passe. Ce coût est contrôlé par des paramètres tels que le nombre d'itérations (pour PBKDF2) ou le facteur de coût (pour bcrypt et scrypt).

Revisitons notre script kdf_demo.py et modifions-le pour inclure le hachage bcrypt et scrypt, et observons le temps nécessaire pour chacun.

Tout d'abord, installez la bibliothèque passlib, qui fournit des implémentations pour divers KDFs :

pip install passlib

Maintenant, modifiez kdf_demo.py pour inclure le hachage bcrypt et scrypt.

nano kdf_demo.py

Remplacez le contenu existant par le code suivant :

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

password = "mysecretpassword"

print("--- PBKDF2-SHA256 ---")
start_time = time.time()
## Le nombre d'itérations par défaut pour pbkdf2_sha256 dans passlib est 29000
pbkdf2_hash = pbkdf2_sha256.hash(password)
end_time = time.time()
print(f"Hash: {pbkdf2_hash}")
print(f"Temps pris : {end_time - start_time:.4f} secondes")
print(f"Itérations : {pbkdf2_sha256.identify(pbkdf2_hash).get('rounds')}")
print("-" * 20)

print("--- bcrypt ---")
start_time = time.time()
## Le nombre de tours par défaut pour bcrypt dans passlib est 12
bcrypt_hash = bcrypt.hash(password)
end_time = time.time()
print(f"Hash: {bcrypt_hash}")
print(f"Temps pris : {end_time - start_time:.4f} secondes")
print(f"Facteur de coût (tours) : {bcrypt.identify(bcrypt_hash).get('rounds')}")
print("-" * 20)

print("--- scrypt ---")
start_time = time.time()
## Les paramètres par défaut pour scrypt dans passlib sont N=2^14, r=8, p=1
scrypt_hash = scrypt.hash(password)
end_time = time.time()
print(f"Hash: {scrypt_hash}")
print(f"Temps pris : {end_time - start_time:.4f} secondes")
print(f"N (coût CPU/mémoire) : {scrypt.identify(scrypt_hash).get('N')}")
print(f"r (Taille du bloc) : {scrypt.identify(scrypt_hash).get('r')}")
print(f"p (Parallélisation) : {scrypt.identify(scrypt_hash).get('p')}")
print("-" * 20)

Enregistrez le fichier en appuyant sur Ctrl+X, puis Y, puis Entrée.

Maintenant, exécutez le script Python mis à jour :

python3 kdf_demo.py

Observez la sortie. Vous verrez le hachage généré par chaque KDF et le temps qu'il a fallu pour le générer.

--- 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>
--------------------

Vous remarquerez que même pour un seul hachage, cela prend un temps mesurable (par exemple, des millisecondes). C'est le coût computationnel intentionnel. Si un attaquant essaie de casser un mot de passe en devinant des millions ou des milliards de mots de passe, ce léger délai par hachage s'accumule considérablement, rendant les attaques par force brute impraticables.

Par exemple, si un KDF prend 0,1 seconde pour hacher un mot de passe, un attaquant essayant 1000 suppositions par seconde ne pourrait tester que 10 mots de passe par seconde. Cela ralentit considérablement le processus de cassage par rapport aux algorithmes de hachage simples et rapides.

Les paramètres (itérations, facteur de coût, N, r, p) peuvent être ajustés pour augmenter ou diminuer ce coût computationnel. À mesure que la puissance de calcul augmente avec le temps, ces paramètres doivent être augmentés pour maintenir le même niveau de sécurité.

Implémenter les KDFs pour le Stockage Sécurisé des Mots de Passe

Dans cette étape, vous apprendrez comment implémenter les KDFs pour le stockage sécurisé des mots de passe dans un scénario pratique. L'objectif est de stocker les hachages de mots de passe, et non les mots de passe eux-mêmes, et d'utiliser les KDFs pour rendre ces hachages résistants au cassage. Cela implique de générer un sel unique pour chaque mot de passe, de hacher le mot de passe avec le KDF et le sel choisis, et de stocker le hachage résultant (qui inclut le sel et les paramètres du KDF) dans une base de données ou un fichier.

Nous continuerons à utiliser Python et la bibliothèque passlib pour simuler un système simple d'enregistrement et de connexion d'utilisateurs.

Tout d'abord, assurez-vous d'être dans le répertoire ~/project.

cd ~/project

Maintenant, créez un nouveau script Python nommé secure_auth.py :

nano secure_auth.py

Ajoutez le code Python suivant au fichier. Ce script vous permettra d'enregistrer un nouvel utilisateur avec un mot de passe (qui sera haché en utilisant bcrypt) puis de vérifier une tentative de connexion.

from passlib.hash import bcrypt

## Simuler une base de données d'utilisateurs
## Dans une application réelle, il s'agirait d'une base de données (par exemple, SQLite, PostgreSQL)
user_db = {}

def register_user(username, password):
    """Hache le mot de passe en utilisant bcrypt et le stocke."""
    if username in user_db:
        print(f"Erreur : L'utilisateur '{username}' existe déjà.")
        return False

    ## Hache le mot de passe en utilisant bcrypt. Passlib gère la génération du sel et le facteur de coût.
    hashed_password = bcrypt.hash(password)
    user_db[username] = hashed_password
    print(f"Utilisateur '{username}' enregistré avec succès.")
    print(f"Hachage stocké : {hashed_password}")
    return True

def verify_login(username, password):
    """Vérifie un mot de passe par rapport au hachage stocké."""
    if username not in user_db:
        print(f"Échec de la connexion : Utilisateur '{username}' introuvable.")
        return False

    stored_hash = user_db[username]

    ## Vérifie le mot de passe en utilisant bcrypt.verify().
    ## Cette fonction extrait automatiquement le sel et le coût du hachage.
    if bcrypt.verify(password, stored_hash):
        print(f"Connexion réussie pour l'utilisateur '{username}'.")
        return True
    else:
        print(f"Échec de la connexion : Mot de passe incorrect pour l'utilisateur '{username}'.")
        return False

## --- Démonstration ---
print("--- Enregistrement des utilisateurs ---")
register_user("alice", "securepassword123")
register_user("bob", "anothersecret")
register_user("alice", "duplicateuser") ## Tentative d'enregistrement d'un utilisateur existant

print("\n--- Tentatives de connexion ---")
verify_login("alice", "securepassword123") ## Mot de passe correct
verify_login("bob", "wrongpassword")      ## Mot de passe incorrect
verify_login("charlie", "anypassword")    ## Utilisateur inexistant
verify_login("bob", "anothersecret")     ## Mot de passe correct

Enregistrez le fichier en appuyant sur Ctrl+X, puis Y, puis Entrée.

Maintenant, exécutez le script secure_auth.py :

python3 secure_auth.py

Observez la sortie. Vous verrez le processus d'enregistrement, y compris les hachages bcrypt générés, et les résultats des tentatives de connexion.

--- 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'.

Ce script démontre les principes fondamentaux du stockage sécurisé des mots de passe à l'aide des KDFs :

  1. Hachage lors de l'enregistrement : Lorsqu'un utilisateur s'enregistre, son mot de passe en clair n'est jamais stocké. Au lieu de cela, il est immédiatement haché à l'aide d'un KDF (bcrypt dans ce cas), et le hachage résultant est stocké. La fonction bcrypt.hash() gère automatiquement la génération du sel et applique le facteur de coût par défaut.
  2. Vérification lors de la connexion : Lorsqu'un utilisateur tente de se connecter, son mot de passe fourni est à nouveau haché en utilisant le même KDF et les mêmes paramètres (sel et facteur de coût) extraits du hachage stocké. Le nouveau hachage généré est ensuite comparé au hachage stocké. S'ils correspondent, la connexion est réussie. La fonction bcrypt.verify() simplifie ce processus.

En utilisant des KDFs, même si un attaquant accède à votre user_db (ou à une vraie base de données), il n'aura accès qu'aux hachages coûteux en calcul, ce qui rend la récupération des mots de passe d'origine considérablement plus difficile et plus lente.

Résumé

Dans ce laboratoire, vous avez acquis une compréhension approfondie des Fonctions de Dérivation de Clés (KDFs) et de leur rôle essentiel dans le stockage sécurisé des mots de passe. Vous avez découvert des KDFs populaires tels que PBKDF2, bcrypt et scrypt, en reconnaissant leurs formats de hachage uniques et les paramètres qui contrôlent leur coût computationnel. Vous avez observé comment John the Ripper, un puissant outil de cassage de mots de passe, prend en charge ces KDFs, soulignant l'importance de mots de passe robustes même lorsque des KDFs sont utilisés. Enfin, vous avez implémenté un système de stockage sécurisé de mots de passe de base en utilisant Python et la bibliothèque passlib, démontrant l'application pratique des KDFs pour le hachage et la vérification des mots de passe. Ce laboratoire a renforcé le principe selon lequel les KDFs sont essentiels pour rendre les attaques par force brute et par dictionnaire infaisables sur le plan computationnel, améliorant ainsi considérablement la sécurité des identifiants des utilisateurs.