Einleitung
In diesem Labor werden Sie Key Derivation Functions (KDFs) untersuchen, kryptografische Algorithmen, die darauf ausgelegt sind, Brute-Force-Angriffe auf Passwörter zu erschweren. Im Gegensatz zu einfachen Hashing-Funktionen führen KDFs bewusst einen Rechenaufwand ein, der das Testen vieler Passwortvermutungen erheblich verlangsamt. Sie lernen gängige KDFs wie PBKDF2, bcrypt und scrypt kennen, verstehen, wie diese eindeutige Passwort-Hashes generieren, und beobachten, wie ein leistungsstarkes Passwort-Cracking-Tool wie John the Ripper mit diesen Funktionen interagiert. Abschließend sammeln Sie praktische Erfahrungen bei der Implementierung von KDFs für die sichere Speicherung von Passwörtern und festigen bewährte Praktiken in der Cybersicherheit.
KDFs verstehen (z. B. PBKDF2, bcrypt, scrypt)
In diesem Schritt erhalten Sie ein grundlegendes Verständnis von Key Derivation Functions (KDFs). KDFs sind kryptografische Algorithmen, die einen oder mehrere geheime Schlüssel aus einem geheimen Wert wie einem Master-Schlüssel oder einem Passwort ableiten. Ihr Hauptzweck bei der Speicherung von Passwörtern ist es, Brute-Force- und Wörterbuchangriffe rechenintensiv zu machen, selbst wenn ein Angreifer die gehashten Passwörter erhält. Dies wird erreicht, indem der Hashing-Prozess durch iterative Berechnungen und die Verwendung eines "Salts" (Salz) absichtlich verlangsamt wird.
Ein "Salt" ist eine zufällige Zeichenkette von Daten, die für jedes Passwort eindeutig ist. Wenn ein Passwort gehasht wird, wird der Salt vor dem Hashing mit dem Passwort kombiniert. Dies verhindert, dass Angreifer vortrainierte Rainbow Tables verwenden, um Passwörter zu knacken, und stellt sicher, dass zwei identische Passwörter unterschiedliche Hashes erzeugen, wenn ihre Salts unterschiedlich sind.
Lassen Sie uns einige gängige KDFs untersuchen:
- PBKDF2 (Password-Based Key Derivation Function 2): Diese Funktion wendet eine pseudozufällige Funktion (wie HMAC-SHA256) auf das Eingabepasswort zusammen mit einem Salt an und wiederholt den Vorgang viele Male (Iterationen), um die Rechenkosten zu erhöhen.
- bcrypt: Basierend auf dem Blowfish-Chiffre ist bcrypt adaptiv konzipiert, was bedeutet, dass seine Rechenkosten im Laufe der Zeit erhöht werden können, um mit steigenden Prozessorgeschwindigkeiten Schritt zu halten. Es ist bekannt für seine Widerstandsfähigkeit gegen GPU-basierte Angriffe.
- scrypt: Diese KDF wurde speziell entwickelt, um widerstandsfähig gegen Hardware-Angriffe (ASICs und FPGAs) zu sein, indem sie neben Rechenleistung auch erhebliche Mengen an Speicher benötigt.
Um zu beginnen, stellen wir sicher, dass John the Ripper installiert ist, da wir es später zur Demonstration von KDFs verwenden werden.
john --version
Sie sollten eine Ausgabe sehen, die der folgenden ähnelt und anzeigt, dass John the Ripper installiert ist:
John the Ripper password cracker, version 1.9.0-jumbo-1+bleeding-e7022e5 64-bit
Copyright (c) 1996-2020 by Solar Designer
...
Als Nächstes erstellen wir ein einfaches Python-Skript, um zu demonstrieren, wie PBKDF2 zum Hashing eines Passworts verwendet werden kann.
Erstellen Sie eine Datei namens kdf_demo.py:
nano kdf_demo.py
Fügen Sie den folgenden Python-Code in die Datei ein:
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")
Speichern Sie die Datei, indem Sie Ctrl+X, dann Y und dann Enter drücken.
Führen Sie nun das Python-Skript aus:
python3 kdf_demo.py
Sie sehen eine Ausgabe, die das ursprüngliche Passwort, den generierten Salt und den PBKDF2-Hash anzeigt. Beachten Sie, dass der Salt eine zufällige Hexadezimalzeichenkette ist und der Hash ebenfalls eine Hexadezimalzeichenkette ist. Jedes Mal, wenn Sie das Skript ausführen, wird ein neuer Salt generiert, was zu einem anderen Hash für dasselbe Passwort führt.
Password: mysecretpassword
Salt (hex): <random_hex_string>
Hashed Password (hex): <random_hex_string>
Iterations: 100000
Dies demonstriert das Kernkonzept von KDFs: die Kombination eines Passworts mit einem eindeutigen Salt und die Anwendung vieler Iterationen einer Hashing-Funktion, um einen rechenintensiven Hash zu erzeugen.
Generierte Hashes von KDFs identifizieren
In diesem Schritt lernen Sie, das Format von Hashes zu identifizieren, die von verschiedenen KDFs generiert werden. Während die Rohausgabe eines KDFs eine binäre Zeichenkette sein kann, werden sie bei der Speicherung in Systemen oft kodiert (z. B. Base64 oder Hexadezimal) und mit Präfixen versehen, die das verwendete KDF, den Salt und manchmal die Iterationsanzahl oder den Kostenfaktor angeben. Dieses standardisierte Format ermöglicht es Tools wie John the Ripper, sie korrekt zu erkennen und zu verarbeiten.
Betrachten wir gängige Hash-Formate für PBKDF2, bcrypt und scrypt.
PBKDF2:
PBKDF2-Hashes erscheinen oft in einem Format, das den Algorithmus, die Iterationen, den Salt und den abgeleiteten Schlüssel enthält. Zum Beispiel könnten PBKDF2 (speziell SHA512)-Hashes in /etc/shadow-Dateien unter Linux wie folgt aussehen:
$6$rounds=5000$<salt>$<hash>
Hier gibt $6$ SHA-512 an, rounds= spezifiziert die Iterationen, gefolgt vom Salt und dem eigentlichen Hash.
bcrypt:
bcrypt-Hashes sind leicht an ihrem Präfix $2a$, $2b$ oder $2y$ zu erkennen, gefolgt vom Kostenfaktor (z. B. 10), dem Salt und dem Hash.
Beispiel: $2a$10$<salt><hash>
scrypt:
scrypt-Hashes beginnen typischerweise mit $7$ oder $scrypt$, gefolgt von Parametern wie ln, r, p (logarithmischer Kostenfaktor, Blockgröße und Parallelisierungsfaktor), dem Salt und dem Hash.
Beispiel: $7$<ln>$<r>$<p>$<salt><hash>
Zur Demonstration erstellen wir eine Datei, die einige Beispiel-KDF-Hashes enthält. Wir verwenden ein Tool namens mkpasswd (Teil des Pakets whois), um einen bcrypt-Hash zu generieren, und erstellen dann manuell einen PBKDF2-Hash zur Demonstration.
Installieren Sie zuerst das Paket whois, um mkpasswd zu erhalten:
sudo apt install -y whois
Nun generieren wir einen bcrypt-Hash für das Passwort "password123" mit einem Kostenfaktor von 10.
mkpasswd -m bcrypt -S $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16) -s 10 password123
Dieser Befehl generiert einen bcrypt-Hash. Die Option -S liefert einen zufälligen Salt, und -s 10 setzt den Kostenfaktor auf 10. Die Ausgabe ist eine bcrypt-Hash-Zeichenkette.
$2a$10$<random_salt_string><hash_string>
Erstellen wir nun eine Datei namens kdf_hashes.txt, die John the Ripper lesen kann. Wir fügen einen bcrypt-Hash und einen manuell erstellten PBKDF2-Hash hinzu.
nano kdf_hashes.txt
Fügen Sie den folgenden Inhalt in die Datei ein. Ersetzen Sie <YOUR_GENERATED_BCRYPT_HASH> durch den tatsächlichen bcrypt-Hash, den Sie im vorherigen Schritt generiert haben.
user1:$2a$10$<YOUR_GENERATED_BCRYPT_HASH>
user2:$pbkdf2-sha256$100000$c0ffee$a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef
Hinweis: Der PBKDF2-Hash für user2 ist ein Platzhalter zu Demonstrationszwecken. Es ist kein echter Hash eines bekannten Passworts, aber er folgt dem Format, das John the Ripper für PBKDF2-SHA256 erwartet. Das Format ist $pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>.
Speichern Sie die Datei, indem Sie Ctrl+X, dann Y und dann Enter drücken.
Nun verwenden wir John the Ripper, um die Hash-Typen in kdf_hashes.txt zu identifizieren:
john --format=raw-md5 --show kdf_hashes.txt
Hinweis: Wir verwenden --format=raw-md5 als Dummy-Format, da John ein Format für --show angeben muss, selbst wenn es nur um die Identifizierung geht. John erkennt die tatsächlichen KDF-Formate automatisch.
Die Ausgabe zeigt, wie John die Hash-Typen identifiziert:
0 password hashes cracked, 2 left
John erkennt korrekt, dass es zwei Hashes gibt, und wird deren KDF-Typen erkennen, wenn er versucht, sie zu knacken. Dieser Schritt konzentriert sich hauptsächlich auf die Erkennung der Hash-Formate.
John the Rippers Unterstützung für KDFs beobachten
In diesem Schritt werden Sie beobachten, wie John the Ripper das Knacken von Hashes unterstützt, die von KDFs generiert werden. John the Ripper verfügt über integrierte Unterstützung für eine breite Palette von Hash-Typen, einschließlich verschiedener KDFs wie PBKDF2, bcrypt und scrypt. Wenn Sie John eine Datei mit diesen Hashes zur Verfügung stellen, erkennt es automatisch den Hash-Typ und wendet die entsprechenden Knack-Algorithmen an.
Wir werden die im vorherigen Schritt erstellte Datei kdf_hashes.txt verwenden. Wir werden versuchen, den user1-Hash (bcrypt) mit einer kleinen Wortliste zu knacken.
Zuerst erstellen wir eine kleine Wortlistendatei namens wordlist.txt, die gängige Passwörter enthält, einschließlich "password123".
nano wordlist.txt
Fügen Sie den folgenden Inhalt zu wordlist.txt hinzu:
test
123456
password
password123
qwerty
Speichern Sie die Datei, indem Sie Ctrl+X, dann Y und dann Enter drücken.
Nun verwenden wir John the Ripper, um die Hashes in kdf_hashes.txt mit unserer wordlist.txt zu knacken.
john kdf_hashes.txt --wordlist=wordlist.txt
John beginnt den Knack-Prozess. Da "password123" in unserer Wortliste enthalten ist und der bcrypt-Hash für user1 daraus generiert wurde, sollte John ihn schnell knacken.
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.
Sie können sehen, dass John user1 erfolgreich geknackt und das Passwort als password123 identifiziert hat. Der user2-Hash (PBKDF2) wurde nicht geknackt, da sein "Passwort" nicht in unserer Wortliste enthalten war (es war ein Platzhalter-Hash).
Um die geknackten Passwörter anzuzeigen, können Sie die Option --show verwenden:
john --show kdf_hashes.txt
Die Ausgabe zeigt das geknackte Passwort für user1:
user1:password123
1 password hash cracked, 1 left
Dies demonstriert die Fähigkeit von John the Ripper, KDF-Hashes automatisch zu erkennen und zu knacken, und unterstreicht die Bedeutung der Verwendung starker, eindeutiger Passwörter, auch mit KDFs.
Die Rechenkosten von KDFs verstehen
In diesem Schritt erhalten Sie ein tieferes Verständnis für die mit KDFs verbundenen Rechenkosten und warum dies ein entscheidendes Sicherheitsmerkmal ist. Das Hauptziel von KDFs ist es, das Knacken von Passwörtern rechenintensiv zu gestalten und somit die Zeit und die Ressourcen zu erhöhen, die ein Angreifer benötigt, um Passwörter zu erraten. Diese Kosten werden durch Parameter wie die Anzahl der Iterationen (für PBKDF2) oder den Kostenfaktor (für bcrypt und scrypt) gesteuert.
Lassen Sie uns unser Skript kdf_demo.py erneut aufgreifen und es modifizieren, um bcrypt- und scrypt-Hashing einzuschließen und die dafür benötigte Zeit zu beobachten.
Installieren Sie zuerst die Bibliothek passlib, die Implementierungen für verschiedene KDFs bereitstellt:
pip install passlib
Modifizieren Sie nun kdf_demo.py, um bcrypt- und scrypt-Hashing einzuschließen.
nano kdf_demo.py
Ersetzen Sie den vorhandenen Inhalt durch den folgenden Code:
import time
from passlib.hash import pbkdf2_sha256, bcrypt, scrypt
password = "mysecretpassword"
print("--- PBKDF2-SHA256 ---")
start_time = time.time()
## Standard-Iterationen für pbkdf2_sha256 in passlib sind 29000
pbkdf2_hash = pbkdf2_sha256.hash(password)
end_time = time.time()
print(f"Hash: {pbkdf2_hash}")
print(f"Benötigte Zeit: {end_time - start_time:.4f} Sekunden")
print(f"Iterationen: {pbkdf2_sha256.identify(pbkdf2_hash).get('rounds')}")
print("-" * 20)
print("--- bcrypt ---")
start_time = time.time()
## Standard-Runden für bcrypt in passlib sind 12
bcrypt_hash = bcrypt.hash(password)
end_time = time.time()
print(f"Hash: {bcrypt_hash}")
print(f"Benötigte Zeit: {end_time - start_time:.4f} Sekunden")
print(f"Kostenfaktor (Runden): {bcrypt.identify(bcrypt_hash).get('rounds')}")
print("-" * 20)
print("--- scrypt ---")
start_time = time.time()
## Standard-Parameter für scrypt in passlib sind N=2^14, r=8, p=1
scrypt_hash = scrypt.hash(password)
end_time = time.time()
print(f"Hash: {scrypt_hash}")
print(f"Benötigte Zeit: {end_time - start_time:.4f} Sekunden")
print(f"N (CPU/Speicherkosten): {scrypt.identify(scrypt_hash).get('N')}")
print(f"r (Blockgröße): {scrypt.identify(scrypt_hash).get('r')}")
print(f"p (Parallelisierung): {scrypt.identify(scrypt_hash).get('p')}")
print("-" * 20)
Speichern Sie die Datei, indem Sie Ctrl+X, dann Y und dann Enter drücken.
Führen Sie nun das aktualisierte Python-Skript aus:
python3 kdf_demo.py
Beobachten Sie die Ausgabe. Sie sehen den von jedem KDF generierten Hash und die Zeit, die für dessen Generierung benötigt wurde.
--- PBKDF2-SHA256 ---
Hash: $pbkdf2-sha256$<iterations>$<salt>$<hash>
Benötigte Zeit: <time> Sekunden
Iterationen: <iterations_value>
--------------------
--- bcrypt ---
Hash: $2a$<cost_factor>$<salt_and_hash>
Benötigte Zeit: <time> Sekunden
Kostenfaktor (Runden): <cost_factor_value>
--------------------
--- scrypt ---
Hash: $scrypt$<N>$<r>$<p>$<salt_and_hash>
Benötigte Zeit: <time> Sekunden
N (CPU/Speicherkosten): <N_value>
r (Blockgröße): <r_value>
p (Parallelisierung): <p_value>
--------------------
Sie werden feststellen, dass selbst für einen einzelnen Hash eine messbare Zeit (z. B. Millisekunden) benötigt wird. Dies sind die beabsichtigten Rechenkosten. Wenn ein Angreifer versucht, ein Passwort zu knacken, indem er Millionen oder Milliarden von Passwörtern errät, summiert sich diese geringe Verzögerung pro Hash erheblich, was Brute-Force-Angriffe unpraktisch macht.
Wenn beispielsweise ein KDF 0,1 Sekunden benötigt, um ein Passwort zu hashen, kann ein Angreifer, der 1000 Vermutungen pro Sekunde versucht, nur 10 Passwörter pro Sekunde testen. Dies verlangsamt den Knack-Prozess im Vergleich zu einfachen, schnellen Hashing-Algorithmen drastisch.
Die Parameter (Iterationen, Kostenfaktor, N, r, p) können angepasst werden, um diese Rechenkosten zu erhöhen oder zu verringern. Da die Rechenleistung im Laufe der Zeit zunimmt, sollten diese Parameter erhöht werden, um das gleiche Sicherheitsniveau aufrechtzuerhalten.
KDFs für sichere Passwortspeicherung implementieren
In diesem Schritt lernen Sie, wie Sie KDFs für die sichere Passwortspeicherung in einem praktischen Szenario implementieren. Das Ziel ist es, Passwort-Hashes zu speichern, nicht die Passwörter selbst, und KDFs zu verwenden, um diese Hashes widerstandsfähig gegen das Knacken zu machen. Dies beinhaltet die Generierung eines eindeutigen Salts für jedes Passwort, das Hashing des Passworts mit dem gewählten KDF und Salt sowie die Speicherung des resultierenden Hashes (der den Salt und die KDF-Parameter enthält) in einer Datenbank oder Datei.
Wir werden weiterhin Python und die Bibliothek passlib verwenden, um ein einfaches Benutzerregistrierungs- und Anmeldesystem zu simulieren.
Stellen Sie zunächst sicher, dass Sie sich im Verzeichnis ~/project befinden.
cd ~/project
Erstellen Sie nun ein neues Python-Skript namens secure_auth.py:
nano secure_auth.py
Fügen Sie den folgenden Python-Code in die Datei ein. Dieses Skript ermöglicht es Ihnen, einen neuen Benutzer mit einem Passwort zu registrieren (das mit bcrypt gehasht wird) und dann einen Anmeldeversuch zu überprüfen.
from passlib.hash import bcrypt
## Simulierte Benutzerdatenbank
## In einer echten Anwendung wäre dies eine Datenbank (z. B. SQLite, PostgreSQL)
user_db = {}
def register_user(username, password):
"""Hasht das Passwort mit bcrypt und speichert es."""
if username in user_db:
print(f"Fehler: Benutzer '{username}' existiert bereits.")
return False
## Passwort mit bcrypt hashen. Passlib kümmert sich um die Salt-Generierung und den Kostenfaktor.
hashed_password = bcrypt.hash(password)
user_db[username] = hashed_password
print(f"Benutzer '{username}' erfolgreich registriert.")
print(f"Gespeicherter Hash: {hashed_password}")
return True
def verify_login(username, password):
"""Überprüft ein Passwort gegen den gespeicherten Hash."""
if username not in user_db:
print(f"Anmeldung fehlgeschlagen: Benutzer '{username}' nicht gefunden.")
return False
stored_hash = user_db[username]
## Passwort mit bcrypt.verify() überprüfen.
## Diese Funktion extrahiert automatisch Salt und Kosten aus dem Hash.
if bcrypt.verify(password, stored_hash):
print(f"Anmeldung für Benutzer '{username}' erfolgreich.")
return True
else:
print(f"Anmeldung fehlgeschlagen: Falsches Passwort für Benutzer '{username}'.")
return False
## --- Demonstration ---
print("--- Registrierung von Benutzern ---")
register_user("alice", "securepassword123")
register_user("bob", "anothersecret")
register_user("alice", "duplicateuser") ## Versuch, einen bestehenden Benutzer zu registrieren
print("\n--- Anmeldeversuche ---")
verify_login("alice", "securepassword123") ## Korrektes Passwort
verify_login("bob", "wrongpassword") ## Falsches Passwort
verify_login("charlie", "anypassword") ## Nicht existierender Benutzer
verify_login("bob", "anothersecret") ## Korrektes Passwort
Speichern Sie die Datei, indem Sie Ctrl+X, dann Y und dann Enter drücken.
Führen Sie nun das Skript secure_auth.py aus:
python3 secure_auth.py
Beobachten Sie die Ausgabe. Sie sehen den Registrierungsprozess, einschließlich der generierten bcrypt-Hashes, und die Ergebnisse der Anmeldeversuche.
--- Registrierung von Benutzern ---
Benutzer 'alice' erfolgreich registriert.
Gespeicherter Hash: $2a$<cost_factor>$<salt_and_hash>
Benutzer 'bob' erfolgreich registriert.
Gespeicherter Hash: $2a$<cost_factor>$<salt_and_hash>
Fehler: Benutzer 'alice' existiert bereits.
--- Anmeldeversuche ---
Anmeldung für Benutzer 'alice' erfolgreich.
Anmeldung fehlgeschlagen: Falsches Passwort für Benutzer 'bob'.
Anmeldung fehlgeschlagen: Benutzer 'charlie' nicht gefunden.
Anmeldung für Benutzer 'bob' erfolgreich.
Dieses Skript demonstriert die grundlegenden Prinzipien der sicheren Passwortspeicherung mit KDFs:
- Hashing bei der Registrierung: Wenn sich ein Benutzer registriert, wird sein Klartext-Passwort niemals gespeichert. Stattdessen wird es sofort mit einem KDF (in diesem Fall bcrypt) gehasht, und der resultierende Hash wird gespeichert. Die Funktion
bcrypt.hash()kümmert sich automatisch um die Salt-Generierung und wendet den Standard-Kostenfaktor an. - Überprüfung bei der Anmeldung: Wenn sich ein Benutzer anmeldet, wird sein bereitgestelltes Passwort erneut mit demselben KDF und denselben Parametern (Salt und Kostenfaktor) gehasht, die aus dem gespeicherten Hash extrahiert wurden. Der neu generierte Hash wird dann mit dem gespeicherten Hash verglichen. Wenn sie übereinstimmen, ist die Anmeldung erfolgreich. Die Funktion
bcrypt.verify()vereinfacht diesen Prozess.
Durch die Verwendung von KDFs haben Angreifer, selbst wenn sie Zugriff auf Ihre user_db (oder eine echte Datenbank) erhalten, nur Zugriff auf die rechenintensiven Hashes, was die Wiederherstellung der ursprünglichen Passwörter erheblich erschwert und verlangsamt.
Zusammenfassung
In diesem Lab haben Sie ein umfassendes Verständnis von Key Derivation Functions (KDFs) und ihrer entscheidenden Rolle bei der sicheren Passwortspeicherung erlangt. Sie haben sich mit gängigen KDFs wie PBKDF2, bcrypt und scrypt vertraut gemacht und deren einzigartige Hash-Formate sowie die Parameter, die ihre Rechenkosten steuern, kennengelernt. Sie haben beobachtet, wie John the Ripper, ein leistungsstarkes Werkzeug zum Knacken von Passwörtern, diese KDFs unterstützt, was die Bedeutung starker Passwörter auch bei der Verwendung von KDFs unterstreicht. Schließlich haben Sie ein grundlegendes System zur sicheren Passwortspeicherung mit Python und der passlib-Bibliothek implementiert und damit die praktische Anwendung von KDFs zum Hashing und Verifizieren von Passwörtern demonstriert. Dieses Lab hat das Prinzip gefestigt, dass KDFs unerlässlich sind, um Brute-Force- und Wörterbuchangriffe rechnerisch undurchführbar zu machen und somit die Sicherheit von Benutzeranmeldeinformationen erheblich zu verbessern.


