John the Ripper と Key Derivation Functions(KDF)

Kali LinuxBeginner
オンラインで実践に進む

はじめに

この実験(Lab)では、パスワードに対する総当たり攻撃をより困難にするために設計された暗号アルゴリズムである鍵導出関数(KDF: Key Derivation Functions)を探求します。単純なハッシュ関数とは異なり、KDF は意図的に計算コストを導入し、多くのパスワード推測を試す速度を大幅に低下させます。PBKDF2、bcrypt、scrypt などの一般的な KDF について学び、それらがどのようにユニークなパスワードハッシュを生成するかを理解し、John the Ripper のような強力なパスワードクラッキングツールがこれらの関数とどのように相互作用するかを観察します。最後に、安全なパスワードストレージのために KDF を実装する実践的な経験を積み、サイバーセキュリティにおけるベストプラクティスを強化します。

KDF(例:PBKDF2、bcrypt、scrypt)の理解

このステップでは、鍵導出関数(KDF: Key Derivation Functions)の基本的な理解を深めます。KDF は、マスターキーやパスワードなどの秘密の値から 1 つ以上の秘密鍵を導出する暗号アルゴリズムです。パスワードストレージにおける主な目的は、攻撃者がハッシュ化されたパスワードを取得した場合でも、総当たり攻撃や辞書攻撃を計算コストの高いものにすることです。これは、反復計算と「ソルト」の使用を通じて、意図的にハッシュ化プロセスを遅くすることによって達成されます。

「ソルト」とは、各パスワードに対してユニークなランダムなデータ文字列です。パスワードがハッシュ化される際、ソルトはハッシュ化される前にパスワードと組み合わされます。これにより、攻撃者が事前に計算されたレインボーテーブルを使用してパスワードをクラックすることを防ぎ、ソルトが異なる場合、同じパスワードであっても異なるハッシュが生成されることが保証されます。

いくつかの一般的な KDF を見てみましょう。

  • PBKDF2(Password-Based Key Derivation Function 2): この関数は、入力パスワードとソルトを組み合わせて疑似乱数関数(HMAC-SHA256 など)を適用し、計算コストを増大させるためにプロセスを何度も(イテレーション回数)繰り返します。
  • bcrypt: Blowfish 暗号に基づいた bcrypt は、適応型(adaptive)になるように設計されており、プロセッサ速度の向上に対応するために計算コストを時間とともに増加させることができます。GPU ベースの攻撃に対する耐性で知られています。
  • scrypt: この KDF は、計算能力に加えて大量のメモリを必要とすることで、ハードウェア攻撃(ASIC や FPGA)に対する耐性を持つように特別に設計されました。

まず、後で KDF を実証するために使用するため、John the Ripper がインストールされていることを確認しましょう。

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

次に、PBKDF2 を使用してパスワードをハッシュ化する方法を示す簡単な Python スクリプトを作成しましょう。

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 ハッシュを示す出力が表示されます。ソルトはランダムな 16 進数文字列であり、ハッシュも 16 進数文字列であることに注意してください。スクリプトを実行するたびに新しいソルトが生成され、同じパスワードに対して異なるハッシュが生成されます。

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

これは、KDF のコアコンセプトを示しています。パスワードとユニークなソルトを組み合わせ、ハッシュ関数を多数のイテレーションで適用して、計算コストの高いハッシュを生成することです。

KDF によって生成されたハッシュの識別

このステップでは、さまざまな KDF によって生成されたハッシュの形式を識別する方法を学びます。KDF の生の出力はバイナリストリングである可能性がありますが、システムに保存される際には、多くの場合エンコード(例:Base64 または 16 進数)され、使用された 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$で始まり、その後にlnrp(対数コスト、ブロックサイズ、および並列化ファクター)などのパラメータ、ソルト、およびハッシュが続きます。
例:$7$<ln>$<r>$<p>$<salt><hash>

実証のために、いくつかの KDF ハッシュの例を含むファイルを作成しましょう。mkpasswdwhoisパッケージの一部)というツールを使用して bcrypt ハッシュを生成し、その後、デモンストレーションのために PBKDF2 ハッシュを手動で構築します。

まず、mkpasswdを取得するためにwhoisパッケージをインストールします。

sudo apt install -y whois

次に、コストファクターを 10 として、パスワード「password123」の 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>

次に、John the Ripper が読み取れるkdf_hashes.txtという名前のファイルを作成しましょう。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 ハッシュはデモンストレーション目的のプレースホルダーです。既知のパスワードの実際のハッシュではありませんが、PBKDF2-SHA256 に対して John が期待する形式に従っています。形式は$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

注意: --showにはフォーマットを指定する必要があるため、ダミーフォーマットとして--format=raw-md5を使用します。これは識別のためだけであっても必要です。John は実際の KDF フォーマットを自動的に検出します。

出力には、John がハッシュタイプを識別していることが表示されます。

0 password hashes cracked, 2 left

John は 2 つのハッシュがあることを正しく識別し、それらをクラックしようとするときに KDF タイプを認識します。このステップは主にハッシュ形式の認識に焦点を当てています。

John the Ripper による KDF ハッシュのサポートの観察

このステップでは、John the Ripper が KDF によって生成されたハッシュをクラックする方法を観察します。John the Ripper は、PBKDF2、bcrypt、scrypt などのさまざまな KDF を含む、幅広いハッシュタイプをサポートしています。これらのハッシュを含むファイルを John に提供すると、ハッシュタイプが自動的に検出され、適切なクラッキングアルゴリズムが適用されます。

前のステップで作成したkdf_hashes.txtファイルを使用します。簡単な単語リストを使用して、user1のハッシュ(bcrypt)をクラックしようとします。

まず、一般的なパスワード(「password123」を含む)を含むwordlist.txtという小さな単語リストファイルを作成しましょう。

nano wordlist.txt

wordlist.txtに以下のコンテンツを追加します。

test
123456
password
password123
qwerty

ファイルを保存するには、Ctrl+X、次にY、次にEnterを押します。

次に、John the Ripper を使用して、wordlist.txtを使用して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 ハッシュを自動的に検出しクラックする能力を示しており、KDF を使用する場合でも強力でユニークなパスワードを使用することの重要性を強調しています。

KDF の計算コストの理解

このステップでは、KDF に関連する計算コストと、それがなぜ重要なセキュリティ機能であるかをより深く理解します。KDF の主な目的は、パスワードクラッキングを計算上高コストにすることであり、それによって攻撃者がパスワードを推測するために必要な時間とリソースを増加させます。このコストは、イテレーション数(PBKDF2 の場合)やコストファクター(bcrypt および scrypt の場合)などのパラメータによって制御されます。

以前のkdf_demo.pyスクリプトを再確認し、bcrypt と scrypt ハッシュを含めるように変更して、それぞれの所要時間を観察しましょう。

まず、さまざまな KDF の実装を提供するpasslibライブラリをインストールします。

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)は、この計算コストを増減するように調整できます。コンピューティングパワーが時間とともに増加するにつれて、同じレベルのセキュリティを維持するためにこれらのパラメータを増やす必要があります。

安全なパスワードストレージのための KDF の実装

このステップでは、実際のシナリオで安全なパスワードストレージのために KDF を実装する方法を学びます。目標は、パスワード自体ではなくパスワードハッシュを保存し、KDF を使用してこれらのハッシュをクラッキングから保護することです。これには、各パスワードにユニークなソルトを生成し、選択した KDF とソルトでパスワードをハッシュし、結果のハッシュ(ソルトと 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 はソルト生成とコストファクターを処理します。
    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() を使用してパスワードを検証します。
    ## この関数はハッシュからソルトとコストを自動的に抽出します。
    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'.

このスクリプトは、KDF を使用した安全なパスワードストレージの基本原則を示しています。

  1. 登録時のハッシュ化: ユーザーが登録する際、プレーンテキストパスワードは決して保存されません。代わりに、KDF(この場合は bcrypt)を使用してすぐにハッシュ化され、結果のハッシュが保存されます。bcrypt.hash()関数は、ソルト生成を自動的に処理し、デフォルトのコストファクターを適用します。
  2. ログイン時の検証: ユーザーがログインを試みる際、提供されたパスワードは、保存されたハッシュから抽出された同じ KDF とパラメータ(ソルトとコストファクター)を使用して再度ハッシュ化されます。新しく生成されたハッシュは、保存されたハッシュと比較されます。一致した場合、ログインは成功します。bcrypt.verify()関数はこのプロセスを簡素化します。

KDF を使用することで、攻撃者がuser_db(または実際のデータベース)にアクセスできたとしても、計算上高コストなハッシュにしかアクセスできず、元のパスワードを復元することが大幅に困難かつ遅くなります。

まとめ

この実験では、Key Derivation Functions(KDF)とその安全なパスワードストレージにおける重要な役割について包括的に理解しました。PBKDF2、bcrypt、scrypt などの一般的な KDF について学び、それらのユニークなハッシュ形式と計算コストを制御するパラメータを認識しました。強力なパスワードクラッキングツールである John the Ripper がこれらの KDF をサポートしていることを観察し、KDF が使用されている場合でも強力なパスワードの重要性を強調しました。最後に、Python とpasslibライブラリを使用して基本的な安全なパスワードストレージシステムを実装し、パスワードのハッシュ化と検証のための KDF の実用的な応用を実証しました。この実験は、KDF が総当たり攻撃や辞書攻撃を計算上実行不可能にするために不可欠であり、ユーザー認証情報のセキュリティを大幅に向上させるという原則を強化しました。