John the Ripper e Funções de Derivação de Chave (KDFs)

Kali LinuxBeginner
Pratique Agora

Introdução

Neste laboratório, você explorará Funções de Derivação de Chave (KDFs), que são algoritmos criptográficos projetados para tornar ataques de força bruta em senhas mais difíceis. Ao contrário de funções de hash simples, as KDFs introduzem intencionalmente custo computacional, tornando muito mais lento testar muitas suposições de senha. Você aprenderá sobre KDFs comuns como PBKDF2, bcrypt e scrypt, entenderá como elas geram hashes de senha únicos e observará como uma ferramenta poderosa de quebra de senhas como John the Ripper interage com essas funções. Finalmente, você ganhará experiência prática na implementação de KDFs para armazenamento seguro de senhas, reforçando as melhores práticas em cibersegurança.

Compreender KDFs (por exemplo, PBKDF2, bcrypt, scrypt)

Nesta etapa, você obterá uma compreensão fundamental das Funções de Derivação de Chave (KDFs). KDFs são algoritmos criptográficos que derivam uma ou mais chaves secretas de um valor secreto, como uma chave mestra ou uma senha. Seu propósito principal no armazenamento de senhas é tornar os ataques de força bruta e de dicionário computacionalmente caros, mesmo que um invasor obtenha as senhas hasheadas. Isso é alcançado retardando intencionalmente o processo de hashing através de computações iterativas e o uso de um "salt".

Um "salt" é uma string aleatória de dados que é única para cada senha. Quando uma senha é hasheada, o salt é combinado com a senha antes do hashing. Isso impede que invasores usem tabelas de arco-íris pré-computadas para quebrar senhas e garante que duas senhas idênticas produzirão hashes diferentes se seus salts forem diferentes.

Vamos explorar algumas KDFs comuns:

  • PBKDF2 (Password-Based Key Derivation Function 2): Esta função aplica uma função pseudorandom (como HMAC-SHA256) à senha de entrada juntamente com um salt e repete o processo muitas vezes (iterações) para aumentar o custo computacional.
  • bcrypt: Baseado no cifrador Blowfish, o bcrypt é projetado para ser adaptável, o que significa que seu custo computacional pode ser aumentado ao longo do tempo para acompanhar o aumento das velocidades do processador. É conhecido por sua resistência a ataques baseados em GPU.
  • scrypt: Esta KDF foi especificamente projetada para ser resistente a ataques de hardware (ASICs e FPGAs), exigindo quantidades significativas de memória, além de poder computacional.

Para começar, vamos garantir que o John the Ripper esteja instalado, pois o usaremos para demonstrar KDFs mais tarde.

john --version

Você deverá ver uma saída semelhante a esta, indicando que o 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
...

Em seguida, vamos criar um script Python simples para demonstrar como o PBKDF2 pode ser usado para hashear uma senha.

Crie um arquivo chamado kdf_demo.py:

nano kdf_demo.py

Adicione o seguinte código Python ao arquivo:

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

Salve o arquivo pressionando Ctrl+X, depois Y, depois Enter.

Agora, execute o script Python:

python3 kdf_demo.py

Você verá uma saída mostrando a senha original, o salt gerado e o hash PBKDF2. Observe que o salt é uma string hexadecimal aleatória, e o hash também é uma string hexadecimal. Cada vez que você executar o script, um novo salt será gerado, resultando em um hash diferente para a mesma senha.

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

Isso demonstra o conceito central das KDFs: combinar uma senha com um salt único e aplicar muitas iterações de uma função de hashing para produzir um hash computacionalmente caro.

Identificar Hashes Gerados por KDFs

Nesta etapa, você aprenderá a identificar o formato dos hashes gerados por diferentes KDFs. Embora a saída bruta de uma KDF possa ser uma string binária, quando armazenados em sistemas, eles são frequentemente codificados (por exemplo, Base64 ou hexadecimal) e prefixados com identificadores que indicam a KDF usada, o salt e, às vezes, a contagem de iterações ou o fator de custo. Este formato padronizado permite que ferramentas como o John the Ripper as reconheçam e processem corretamente.

Vamos analisar os formatos comuns de hash para PBKDF2, bcrypt e scrypt.

PBKDF2:
Os hashes PBKDF2 geralmente aparecem em um formato que inclui o algoritmo, iterações, salt e a chave derivada. Por exemplo, em arquivos /etc/shadow no Linux, os hashes PBKDF2 (especificamente SHA512) podem parecer assim:
$6$rounds=5000$<salt>$<hash>
Aqui, $6$ indica SHA-512, rounds= especifica as iterações, seguido pelo salt e pelo hash real.

bcrypt:
Os hashes bcrypt são facilmente reconhecíveis por seu prefixo $2a$, $2b$ ou $2y$, seguido pelo fator de custo (por exemplo, 10), o salt e o hash.
Exemplo: $2a$10$<salt><hash>

scrypt:
Os hashes scrypt geralmente começam com $7$ ou $scrypt$, seguidos por parâmetros como ln, r, p (custo logarítmico, tamanho do bloco e fator de paralelização), o salt e o hash.
Exemplo: $7$<ln>$<r>$<p>$<salt><hash>

Para demonstrar, vamos criar um arquivo contendo alguns hashes de KDF de exemplo. Usaremos uma ferramenta chamada mkpasswd (que faz parte do pacote whois) para gerar um hash bcrypt e, em seguida, construiremos manualmente um hash PBKDF2 para demonstração.

Primeiro, instale o pacote whois para obter o mkpasswd:

sudo apt install -y whois

Agora, vamos gerar um hash bcrypt para a senha "password123" com um fator de custo de 10.

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

Este comando gera um hash bcrypt. A opção -S fornece um salt aleatório, e -s 10 define o fator de custo para 10. A saída será uma string de hash bcrypt.

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

Agora, vamos criar um arquivo chamado kdf_hashes.txt que o John the Ripper possa ler. Incluiremos um hash bcrypt e um hash PBKDF2 criado manualmente.

nano kdf_hashes.txt

Adicione o seguinte conteúdo ao arquivo. Substitua <YOUR_GENERATED_BCRYPT_HASH> pelo hash bcrypt real que você gerou na etapa anterior.

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

Nota: O hash PBKDF2 para user2 é um placeholder para fins de demonstração. Não é um hash real de uma senha conhecida, mas segue o formato que o John the Ripper espera para PBKDF2-SHA256. O formato é $pbkdf2-sha256$<iterations>$<salt_hex>$<hash_hex>.

Salve o arquivo pressionando Ctrl+X, depois Y, depois Enter.

Agora, vamos usar o John the Ripper para identificar os tipos de hash em kdf_hashes.txt:

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

Nota: Usamos --format=raw-md5 como um formato fictício porque o John requer um formato especificado para --show, mesmo que seja apenas para identificação. O John detectará automaticamente os formatos KDF reais.

A saída mostrará o John identificando os tipos de hash:

0 password hashes cracked, 2 left

O John identifica corretamente que existem dois hashes e reconhecerá seus tipos KDF quando tentar quebrá-los. Esta etapa se concentra principalmente no reconhecimento dos formatos de hash.

Observar o Suporte do John the Ripper a KDFs

Nesta etapa, você observará como o John the Ripper suporta a quebra de hashes gerados por KDFs. O John the Ripper tem suporte integrado para uma ampla gama de tipos de hash, incluindo várias KDFs como PBKDF2, bcrypt e scrypt. Ao fornecer ao John um arquivo contendo esses hashes, ele detecta automaticamente o tipo de hash e aplica os algoritmos de quebra apropriados.

Usaremos o arquivo kdf_hashes.txt criado na etapa anterior. Tentaremos quebrar o hash do user1 (bcrypt) com uma lista de palavras simples.

Primeiro, vamos criar um pequeno arquivo de lista de palavras chamado wordlist.txt contendo senhas comuns, incluindo "password123".

nano wordlist.txt

Adicione o seguinte conteúdo ao wordlist.txt:

test
123456
password
password123
qwerty

Salve o arquivo pressionando Ctrl+X, depois Y, depois Enter.

Agora, vamos usar o John the Ripper para quebrar os hashes em kdf_hashes.txt usando nosso wordlist.txt.

john kdf_hashes.txt --wordlist=wordlist.txt

O John iniciará o processo de quebra. Como "password123" está em nossa lista de palavras e o hash bcrypt para user1 foi gerado a partir dele, o John deverá quebrá-lo rapidamente.

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.

Você pode ver que o John quebrou com sucesso o user1 e identificou a senha como password123. O hash user2 (PBKDF2) não foi quebrado porque sua "senha" não estava em nossa lista de palavras (era um hash placeholder).

Para visualizar as senhas quebradas, você pode usar a opção --show:

john --show kdf_hashes.txt

A saída exibirá a senha quebrada para user1:

user1:password123

1 password hash cracked, 1 left

Isso demonstra a capacidade do John the Ripper de detectar e quebrar automaticamente hashes KDF, destacando a importância de usar senhas fortes e únicas, mesmo com KDFs.

Compreender o Custo Computacional das KDFs

Nesta etapa, você obterá uma compreensão mais profunda do custo computacional associado às KDFs e por que ele é um recurso de segurança crucial. O objetivo principal das KDFs é tornar a quebra de senhas computacionalmente cara, aumentando assim o tempo e os recursos que um atacante precisa para adivinhar senhas. Esse custo é controlado por parâmetros como o número de iterações (para PBKDF2) ou o fator de custo (para bcrypt e scrypt).

Vamos revisitar nosso script kdf_demo.py e modificá-lo para incluir hashing bcrypt e scrypt, e observar o tempo gasto em cada um.

Primeiro, instale a biblioteca passlib, que fornece implementações para várias KDFs:

pip install passlib

Agora, modifique kdf_demo.py para incluir hashing bcrypt e scrypt.

nano kdf_demo.py

Substitua o conteúdo existente pelo seguinte 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)

Salve o arquivo pressionando Ctrl+X, depois Y, depois Enter.

Agora, execute o script Python atualizado:

python3 kdf_demo.py

Observe a saída. Você verá o hash gerado por cada KDF e o tempo que levou para gerá-lo.

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

Você notará que, mesmo para um único hash, leva um tempo mensurável (por exemplo, milissegundos). Este é o custo computacional intencional. Se um atacante tentar quebrar uma senha adivinhando milhões ou bilhões de senhas, esse pequeno atraso por hash se acumula significativamente, tornando os ataques de força bruta impraticáveis.

Por exemplo, se uma KDF leva 0,1 segundos para gerar um hash de senha, um atacante que tenta 1000 adivinhações por segundo só conseguiria testar 10 senhas por segundo. Isso desacelera drasticamente o processo de quebra em comparação com algoritmos de hashing simples e rápidos.

Os parâmetros (iterações, fator de custo, N, r, p) podem ser ajustados para aumentar ou diminuir esse custo computacional. À medida que o poder de computação aumenta ao longo do tempo, esses parâmetros devem ser aumentados para manter o mesmo nível de segurança.

Implementar KDFs para Armazenamento Seguro de Senhas

Nesta etapa, você aprenderá como implementar KDFs para armazenamento seguro de senhas em um cenário prático. O objetivo é armazenar hashes de senhas, não as senhas em si, e usar KDFs para tornar esses hashes resistentes à quebra. Isso envolve gerar um salt único para cada senha, fazer o hash da senha com a KDF e o salt escolhidos, e armazenar o hash resultante (que inclui o salt e os parâmetros da KDF) em um banco de dados ou arquivo.

Continuaremos usando Python e a biblioteca passlib para simular um sistema simples de registro e login de usuários.

Primeiro, certifique-se de estar no diretório ~/project.

cd ~/project

Agora, crie um novo script Python chamado secure_auth.py:

nano secure_auth.py

Adicione o seguinte código Python ao arquivo. Este script permitirá que você registre um novo usuário com uma senha (que será hasheada usando bcrypt) e, em seguida, verifique uma tentativa de login.

from passlib.hash import bcrypt

## Simular um banco de dados de usuários
## Em uma aplicação real, isso seria um banco de dados (por exemplo, SQLite, PostgreSQL)
user_db = {}

def register_user(username, password):
    """Faz o hash da senha usando bcrypt e a armazena."""
    if username in user_db:
        print(f"Erro: Usuário '{username}' já existe.")
        return False

    ## Faz o hash da senha usando bcrypt. A passlib lida com a geração do salt e o fator de custo.
    hashed_password = bcrypt.hash(password)
    user_db[username] = hashed_password
    print(f"Usuário '{username}' registrado com sucesso.")
    print(f"Hash armazenado: {hashed_password}")
    return True

def verify_login(username, password):
    """Verifica uma senha contra o hash armazenado."""
    if username not in user_db:
        print(f"Falha no login: Usuário '{username}' não encontrado.")
        return False

    stored_hash = user_db[username]

    ## Verifica a senha usando bcrypt.verify().
    ## Esta função extrai automaticamente o salt e o custo do hash.
    if bcrypt.verify(password, stored_hash):
        print(f"Login bem-sucedido para o usuário '{username}'.")
        return True
    else:
        print(f"Falha no login: Senha incorreta para o usuário '{username}'.")
        return False

## --- Demonstração ---
print("--- Registrando Usuários ---")
register_user("alice", "securepassword123")
register_user("bob", "anothersecret")
register_user("alice", "duplicateuser") ## Tenta registrar usuário existente

print("\n--- Tentando Logins ---")
verify_login("alice", "securepassword123") ## Senha correta
verify_login("bob", "wrongpassword")      ## Senha incorreta
verify_login("charlie", "anypassword")    ## Usuário inexistente
verify_login("bob", "anothersecret")     ## Senha correta

Salve o arquivo pressionando Ctrl+X, depois Y, depois Enter.

Agora, execute o script secure_auth.py:

python3 secure_auth.py

Observe a saída. Você verá o processo de registro, incluindo os hashes bcrypt gerados, e os resultados das tentativas de login.

--- 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 demonstra os princípios fundamentais do armazenamento seguro de senhas usando KDFs:

  1. Hashing no Registro: Quando um usuário se registra, sua senha em texto plano nunca é armazenada. Em vez disso, ela é imediatamente hasheada usando uma KDF (bcrypt neste caso), e o hash resultante é armazenado. A função bcrypt.hash() lida automaticamente com a geração do salt e aplica o fator de custo padrão.
  2. Verificação no Login: Quando um usuário tenta fazer login, sua senha fornecida é novamente hasheada usando a mesma KDF e parâmetros (salt e fator de custo) extraídos do hash armazenado. O hash recém-gerado é então comparado ao hash armazenado. Se eles corresponderem, o login é bem-sucedido. A função bcrypt.verify() simplifica esse processo.

Ao usar KDFs, mesmo que um atacante obtenha acesso ao seu user_db (ou a um banco de dados real), ele terá acesso apenas aos hashes computacionalmente caros, tornando significativamente mais difícil e lento recuperar as senhas originais.

Resumo

Neste laboratório, você obteve uma compreensão abrangente das Funções de Derivação de Chave (KDFs) e seu papel crítico no armazenamento seguro de senhas. Você aprendeu sobre KDFs populares como PBKDF2, bcrypt e scrypt, reconhecendo seus formatos de hash únicos e os parâmetros que controlam seu custo computacional. Você observou como o John the Ripper, uma poderosa ferramenta de quebra de senhas, suporta essas KDFs, destacando a importância de senhas fortes mesmo quando KDFs são usadas. Finalmente, você implementou um sistema básico de armazenamento seguro de senhas usando Python e a biblioteca passlib, demonstrando a aplicação prática de KDFs para fazer o hash e verificar senhas. Este laboratório reforçou o princípio de que as KDFs são essenciais para tornar os ataques de força bruta e de dicionário computacionalmente inviáveis, aumentando significativamente a segurança das credenciais do usuário.