引言
在本实验中,你将探索密钥派生函数(Key Derivation Functions, KDFs),这是一种旨在增加密码暴力破解攻击难度的加密算法。与简单的哈希函数不同,KDFs 故意引入计算成本,使得测试大量密码猜测的速度大大降低。你将学习 PBKDF2、bcrypt 和 scrypt 等常见的 KDFs,理解它们如何生成独特的密码哈希,并观察像 John the Ripper 这样的强大密码破解工具如何与这些函数交互。最后,你将获得在安全密码存储中实现 KDFs 的实践经验,从而巩固网络安全最佳实践。
理解 KDFs (例如 PBKDF2, bcrypt, scrypt)
在本步骤中,你将对密钥派生函数(Key Derivation Functions, KDFs)有一个基础的了解。KDFs 是加密算法,用于从主密钥或密码等秘密值派生一个或多个密钥。它们在密码存储中的主要目的是使暴力破解和字典攻击在计算上变得昂贵,即使攻击者获得了哈希后的密码。这是通过迭代计算和使用“盐”(salt)来故意减慢哈希过程来实现的。
“盐”是每对密码都独一无二的随机数据字符串。当密码被哈希时,盐会与密码结合后再进行哈希。这可以防止攻击者使用预先计算好的彩虹表来破解密码,并确保如果两个相同密码使用了不同的盐,它们会产生不同的哈希值。
让我们来探索一些常见的 KDFs:
- PBKDF2 (Password-Based Key Derivation Function 2): 该函数将伪随机函数(如 HMAC-SHA256)应用于输入密码和盐,并重复该过程多次(迭代)以增加计算成本。
- bcrypt: 基于 Blowfish 密码,bcrypt 被设计成自适应的,这意味着它的计算成本可以随着时间的推移而增加,以跟上处理器速度的提升。它以其对 GPU 攻击的抵抗力而闻名。
- scrypt: 该 KDF 专门设计用于抵抗硬件攻击(ASICs 和 FPGAs),因为它除了需要计算能力外,还需要大量的内存。
首先,让我们确保 John the Ripper 已安装,因为稍后我们将使用它来演示 KDFs。
john --version
你应该会看到类似以下的输出,表明 John the Ripper 已安装:
John the Ripper password cracker, version 1.9.0-jumbo-1+bleeding-e7022e5 64-bit
Copyright (c) 1996-2020 by Solar Designer
...
接下来,让我们创建一个简单的 Python 脚本来演示如何使用 PBKDF2 来哈希密码。
创建一个名为 kdf_demo.py 的文件:
nano kdf_demo.py
将以下 Python 代码添加到文件中:
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")
通过按 Ctrl+X,然后按 Y,最后按 Enter 来保存文件。
现在,运行 Python 脚本:
python3 kdf_demo.py
你将看到显示原始密码、生成的盐和 PBKDF2 哈希的输出。请注意,盐是一个随机的十六进制字符串,哈希也是一个十六进制字符串。每次运行脚本时,都会生成一个新的盐,导致相同的密码产生不同的哈希。
Password: mysecretpassword
Salt (hex): <random_hex_string>
Hashed Password (hex): <random_hex_string>
Iterations: 100000
这演示了 KDFs 的核心概念:将密码与唯一的盐结合,并应用多次哈希函数迭代以产生计算成本高昂的哈希。
识别 KDFs 生成的哈希
在本步骤中,你将学习识别不同 KDFs 生成的哈希格式。虽然 KDF 的原始输出可能是二进制字符串,但在系统中存储时,它们通常会被编码(例如 Base64 或十六进制),并带有指示所使用的 KDF、盐以及有时迭代次数或成本因子的标识符前缀。这种标准化格式允许像 John the Ripper 这样的工具正确识别和处理它们。
让我们看看 PBKDF2、bcrypt 和 scrypt 的常见哈希格式。
PBKDF2:
PBKDF2 哈希通常出现在包含算法、迭代次数、盐和派生密钥的格式中。例如,在 Linux 的 /etc/shadow 文件中,PBKDF2(特别是 SHA512)哈希可能看起来像这样:
$6$rounds=5000$<salt>$<hash>
这里,$6$ 表示 SHA-512,rounds= 指定迭代次数,后面跟着盐和实际的哈希。
bcrypt:
bcrypt 哈希可以通过其 $2a$, $2b$, 或 $2y$ 前缀轻松识别,后面跟着成本因子(例如 10)、盐和哈希。
示例:$2a$10$<salt><hash>
scrypt:
scrypt 哈希通常以 $7$ 或 $scrypt$ 开头,后面跟着参数,如 ln、r、p(对数成本、块大小和并行化因子)、盐和哈希。
示例:$7$<ln>$<r>$<p>$<salt><hash>
为了演示,让我们创建一个包含几个示例 KDF 哈希的文件。我们将使用一个名为 mkpasswd 的工具(它是 whois 包的一部分)来生成 bcrypt 哈希,然后手动构建一个 PBKDF2 哈希用于演示。
首先,安装 whois 包以获取 mkpasswd:
sudo apt install -y whois
现在,让我们为密码 "password123" 生成一个成本因子为 10 的 bcrypt 哈希。
mkpasswd -m bcrypt -S $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16) -s 10 password123
此命令生成一个 bcrypt 哈希。-S 选项提供一个随机盐,-s 10 将成本因子设置为 10。输出将是一个 bcrypt 哈希字符串。
$2a$10$<random_salt_string><hash_string>
现在,让我们创建一个名为 kdf_hashes.txt 的文件,John the Ripper 可以读取。我们将包含一个 bcrypt 哈希和一个手动构造的 PBKDF2 哈希。
nano kdf_hashes.txt
将以下内容添加到文件中。将 <YOUR_GENERATED_BCRYPT_HASH> 替换为你在上一步生成的实际 bcrypt 哈希。
user1:$2a$10$<YOUR_GENERATED_BCRYPT_HASH>
user2:$pbkdf2-sha256$100000$c0ffee$a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef
注意: user2 的 PBKDF2 哈希是用于演示的占位符。它不是已知密码的真实哈希,但它遵循 John the Ripper 对 PBKDF2-SHA256 期望的格式。格式为 $pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>。
通过按 Ctrl+X,然后按 Y,最后按 Enter 来保存文件。
现在,让我们使用 John the Ripper 来识别 kdf_hashes.txt 中的哈希类型:
john --format=raw-md5 --show kdf_hashes.txt
注意: 我们使用 --format=raw-md5 作为占位符格式,因为 John 要求为 --show 指定一个格式,即使只是为了识别。John 会自动检测实际的 KDF 格式。
输出将显示 John 识别的哈希类型:
0 password hashes cracked, 2 left
John 正确识别出有两个哈希,并且在尝试破解它们时会识别它们的 KDF 类型。此步骤主要侧重于识别哈希格式。
观察 John the Ripper 对 KDFs 的支持
在本步骤中,你将观察 John the Ripper 如何支持破解由 KDFs 生成的哈希。John the Ripper 内置支持多种哈希类型,包括 PBKDF2、bcrypt 和 scrypt 等各种 KDFs。当你提供包含这些哈希的文件给 John 时,它会自动检测哈希类型并应用相应的破解算法。
我们将使用在上一步中创建的 kdf_hashes.txt 文件。我们将尝试使用一个简单的单词列表来破解 user1 的哈希(bcrypt)。
首先,让我们创建一个名为 wordlist.txt 的小型单词列表文件,其中包含常用密码,包括 "password123"。
nano wordlist.txt
将以下内容添加到 wordlist.txt:
test
123456
password
password123
qwerty
通过按 Ctrl+X,然后按 Y,最后按 Enter 来保存文件。
现在,让我们使用 wordlist.txt 来使用 John the Ripper 破解 kdf_hashes.txt 中的哈希。
john kdf_hashes.txt --wordlist=wordlist.txt
John 将开始破解过程。由于 "password123" 在我们的单词列表中,并且 user1 的 bcrypt 哈希就是由此生成的,John 应该会很快破解它。
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.
你可以看到 John 成功破解了 user1 并识别出密码为 password123。user2 的哈希(PBKDF2)未被破解,因为它的“密码”不在我们的单词列表中(它是一个占位符哈希)。
要查看已破解的密码,你可以使用 --show 选项:
john --show kdf_hashes.txt
输出将显示 user1 的已破解密码:
user1:password123
1 password hash cracked, 1 left
这演示了 John the Ripper 自动检测和破解 KDF 哈希的能力,突显了即使使用 KDFs,使用强大且唯一的密码的重要性。
理解 KDFs 的计算成本
在本步骤中,你将更深入地理解与 KDFs 相关的计算成本,以及为什么它是至关重要的安全特性。KDFs 的主要目标是使密码破解在计算上变得昂贵,从而增加攻击者猜测密码所需的时间和资源。这种成本由参数控制,例如迭代次数(对于 PBKDF2)或成本因子(对于 bcrypt 和 scrypt)。
让我们回顾一下 kdf_demo.py 脚本,并修改它以包含 bcrypt 和 scrypt 哈希,并观察每个哈希所需的时间。
首先,安装 passlib 库,它提供了各种 KDFs 的实现:
pip install passlib
现在,修改 kdf_demo.py 以包含 bcrypt 和 scrypt 哈希。
nano kdf_demo.py
用以下代码替换现有内容:
import time
from passlib.hash import pbkdf2_sha256, bcrypt, scrypt
password = "mysecretpassword"
print("--- PBKDF2-SHA256 ---")
start_time = time.time()
## passlib 中 pbkdf2_sha256 的默认迭代次数为 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()
## passlib 中 bcrypt 的默认轮数为 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()
## passlib 中 scrypt 的默认参数为 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)
通过按 Ctrl+X,然后按 Y,最后按 Enter 来保存文件。
现在,运行更新后的 Python 脚本:
python3 kdf_demo.py
观察输出。你将看到每个 KDF 生成的哈希以及生成它所花费的时间。
--- 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>
--------------------
你会注意到,即使是单个哈希,也需要花费可测量的时间(例如,毫秒)。这就是故意的计算成本。如果攻击者试图通过猜测数百万或数十亿个密码来破解密码,那么每个哈希的微小延迟会显著累积,使得暴力破解攻击不切实际。
例如,如果一个 KDF 需要 0.1 秒来哈希一个密码,那么每秒尝试 1000 次猜测的攻击者每秒只能测试 10 个密码。与简单、快速的哈希算法相比,这极大地减慢了破解过程。
参数(迭代次数、成本因子、N、r、p)可以调整以增加或减少此计算成本。随着计算能力的不断提高,应增加这些参数以维持相同的安全级别。
为安全密码存储实现 KDFs
在本步骤中,你将学习如何在实际场景中为安全密码存储实现 KDFs。目标是存储密码的哈希值,而不是密码本身,并使用 KDFs 使这些哈希值能够抵抗破解。这包括为每个密码生成一个唯一的 salt,使用选定的 KDF 和 salt 对密码进行哈希,并将生成的哈希值(包括 salt 和 KDF 参数)存储在数据库或文件中。
我们将继续使用 Python 和 passlib 库来模拟一个简单的用户注册和登录系统。
首先,确保你位于 ~/project 目录。
cd ~/project
现在,创建一个名为 secure_auth.py 的新 Python 脚本:
nano secure_auth.py
将以下 Python 代码添加到文件中。此脚本将允许你使用密码注册一个新用户(该密码将使用 bcrypt 进行哈希),然后验证登录尝试。
from passlib.hash import bcrypt
## 模拟用户数据库
## 在实际应用中,这将是一个数据库(例如 SQLite, PostgreSQL)
user_db = {}
def register_user(username, password):
"""使用 bcrypt 对密码进行哈希并存储。"""
if username in user_db:
print(f"Error: User '{username}' already exists.")
return False
## 使用 bcrypt 对密码进行哈希。Passlib 处理 salt 生成和成本因子。
hashed_password = bcrypt.hash(password)
user_db[username] = hashed_password
print(f"User '{username}' registered successfully.")
print(f"Stored hash: {hashed_password}")
return True
def verify_login(username, password):
"""将密码与存储的哈希进行验证。"""
if username not in user_db:
print(f"Login failed: User '{username}' not found.")
return False
stored_hash = user_db[username]
## 使用 bcrypt.verify() 验证密码。
## 此函数会自动从哈希中提取 salt 和成本因子。
if bcrypt.verify(password, stored_hash):
print(f"Login successful for user '{username}'.")
return True
else:
print(f"Login failed: Incorrect password for user '{username}'.")
return False
## --- 演示 ---
print("--- Registering Users ---")
register_user("alice", "securepassword123")
register_user("bob", "anothersecret")
register_user("alice", "duplicateuser") ## 尝试注册已存在的用户
print("\n--- Attempting Logins ---")
verify_login("alice", "securepassword123") ## 正确密码
verify_login("bob", "wrongpassword") ## 错误密码
verify_login("charlie", "anypassword") ## 不存在的用户
verify_login("bob", "anothersecret") ## 正确密码
通过按 Ctrl+X,然后按 Y,最后按 Enter 来保存文件。
现在,运行 secure_auth.py 脚本:
python3 secure_auth.py
观察输出。你将看到注册过程,包括生成的 bcrypt 哈希,以及登录尝试的结果。
--- 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'.
此脚本演示了使用 KDFs 进行安全密码存储的基本原理:
- 注册时哈希: 当用户注册时,其明文密码永远不会被存储。相反,它会立即使用 KDF(在本例中为 bcrypt)进行哈希,然后将生成的哈希值存储起来。
bcrypt.hash()函数会自动处理 salt 生成并应用默认的成本因子。 - 登录时验证: 当用户尝试登录时,他们提供的密码会再次使用 _相同的 KDF 和参数_(从存储的哈希中提取的 salt 和成本因子)进行哈希。然后将新生成的哈希值与存储的哈希值进行比较。如果匹配,则登录成功。
bcrypt.verify()函数简化了这个过程。
通过使用 KDFs,即使攻击者获得了对你的 user_db(或真实数据库)的访问权限,他们也只能获得计算成本高昂的哈希值,这使得恢复原始密码变得更加困难和缓慢。
总结
在本实验中,你全面理解了密钥派生函数(KDFs)及其在安全密码存储中的关键作用。你了解了 PBKDF2、bcrypt 和 scrypt 等流行的 KDFs,认识了它们独特的哈希格式以及控制其计算成本的参数。你观察了强大的密码破解工具 John the Ripper 如何支持这些 KDFs,强调了即使在使用 KDFs 时,强密码的重要性。最后,你使用 Python 和 passlib 库实现了一个基本的安全密码存储系统,演示了 KDFs 在哈希和验证密码方面的实际应用。本次实验强化了 KDFs 对于使暴力破解和字典攻击在计算上不可行的原则,显著增强了用户凭证的安全性。


