Introducción

Top 10 – Parte 2: Hashing

10 Herramientas de Hashing que Todo Hacker Debe Conocer

El encoding es reversible, el hashing no. Esa es la línea que separa
la representación de datos de la integridad. Un hash no guarda tu contraseña,
guarda su huella digital. No encripta tu archivo, genera su firma.

Entender qué hace cada función hash, cuándo usarla (y cuándo no),
es esencial tanto para atacar como para defender sistemas.

Este es el segundo artículo de la trilogía. El primero cubrió encoding;
el próximo cubrirá encriptación.


¿Qué es hashing y por qué importa?

Una función hash toma cualquier entrada y produce una salida de longitud fija
(llamada hash, resumen o digest). Es unidireccional: dado un hash, es
computacionalmente inviable encontrar la entrada original.

Hashing ≠ Encoding ≠ Encriptación

PropiedadEncodingHashingEncriptación
ReversibleNoSí (con clave)
Longitud salidaVariableFijaVariable
Misma entrada = misma salidaNo (con IV)
Uso principalTransporteIntegridad, passwordsConfidencialidad

Propiedades clave de un hash criptográfico

  • Determinista: misma entrada → mismo hash
  • Rápido de computar: para cualquier entrada
  • Resistente a preimagen: dado un hash, no se encuentra la entrada
  • Resistente a segunda preimagen: dada una entrada, no se encuentra otra con el mismo hash
  • Resistente a colisión: no se encuentran dos entradas con el mismo hash
  • Efecto avalancha: un bit de cambio en la entrada cambia ~50% de los bits de salida

1. MD5 (Message Digest 5)

Longitud: 128 bits (32 caracteres hex). Desarrollado por Ronald Rivest en 1991.
Roto desde 2004 (colisiones demostradas por Wang et al.). Sigue siendo el hash
más ubicuo en sistemas legacy, y el que más aparece en CTFs.

Es rápido, está en todas partes, pero no debe usarse para seguridad.
Colisiones prácticas se generan en segundos con herramientas como hashcoll o md5coll.

Usos en CTF

  • Identificación de tipo de hash: Es el más común en retos de cracking.
    El formato 5d41402abc4b2a76b9719d911017c592 es inconfundible (32 hex).
  • Colisiones de MD5: Retos donde dos strings diferentes deben producir
    el mismo hash para bypassear una validación de integridad. Se genera un
    par colisionante con herramientas como md5coll o hashclash.
  • Hash Length Extension Attack: MD5 (como SHA1 y SHA256) es vulnerable.
    Si el servidor usa hash(secret + mensaje) como MAC, un atacante puede
    extender el mensaje y calcular el nuevo hash sin conocer el secret.
  • Fuerza bruta de contraseñas: Hashes MD5($password) de usuarios.
    Modo hashcat: -m 0.
  • Doble MD5: Algunos retos usan md5(md5($password)). Sigue siendo
    débil porque la cadena no tiene sal.

Usos en el mundo real

  • Verificación de integridad legacy: Descargas de software antiguo que
    aún proporcionan checksums MD5. No es seguro contra atacantes activos
    (pueden generar un archivo malicioso con el mismo MD5 que el legítimo).
  • Almacenamiento de contraseñas (mal práctica): Sistemas que almacenan
    contraseñas como MD5(password) sin sal son vulnerables a ataques de
    rainbow tables y fuerza bruta. El leak de LinkedIn 2012 (6.5M hashes
    SHA1 sin sal) es el caso más famoso.
  • Deduplicación de datos: Sistemas internos que usan MD5 para detectar
    archivos duplicados (donde no hay adversario).
  • Forense: Identificar muestras de malware conocidas por su hash MD5
    en VirusTotal.

🧪 Código Python — MD5

import hashlib

# ─── Generar hash ─────────────────────────────────────────
def md5_hash(data: str) -> str:
    return hashlib.md5(data.encode()).hexdigest()

def md5_file(path: str) -> str:
    h = hashlib.md5()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

# ─── Doble MD5 (común en CTFs) ──────────────────────────
def md5_double(data: str) -> str:
    return md5_hash(md5_hash(data))

# ─── MD5 con sal simple ──────────────────────────────────
def md5_with_salt(password: str, salt: str) -> str:
    return md5_hash(salt + password)

# ─── Verificar (comparación segura contra timing) ────────
import hmac

def md5_verify(data: str, expected_hash: str) -> bool:
    return hmac.compare_digest(md5_hash(data), expected_hash)

# ─── Detectar formato MD5 ────────────────────────────────
import re

def es_md5(cadena: str) -> bool:
    return bool(re.fullmatch(r'[0-9a-fA-F]{32}', cadena))

# ─── Fuerza bruta simple ─────────────────────────────────
def md5_crack(hash_obj: str, wordlist: list) -> str:
    for word in wordlist:
        if md5_hash(word) == hash_obj:
            return word
    return None

2. SHA1 (Secure Hash Algorithm 1)

Longitud: 160 bits (40 caracteres hex). Desarrollado por la NSA en 1995.
Roto desde 2017 (colisión SHAttered demostrada por Google y CWI).

Fue el estándar de hash durante dos décadas. Hoy está en retiro activo,
pero sigue siendo el hash más común en integridad de archivos legacy,
firmas digitales antiguas, y por supuesto, CTFs.

Usos en CTF

  • SHAttered: El reto de colisión SHA1 más famoso. Dos archivos PDF con
    el mismo SHA1 pero contenido radicalmente diferente. Explota la colisión
    para bypassear validación de firma o integridad.
  • Hash Length Extension: SHA1 también es vulnerable al mismo ataque
    que MD5. Misma estructura Merkle-Damgård, misma vulnerabilidad.
  • Firma digital débil: Retos donde un binario o script está firmado y
    la firma SHA1 puede ser falseada por colisión.
  • Identificación: 40 caracteres hex. El segundo hash más común en CTFs
    después de MD5. Modo hashcat: -m 100.
  • Commit de Git: Git usa SHA1 para identificar commits. Hay CTFs que
    explotan colisiones SHA1 para suplantar commits o firmas.

Usos en el mundo real

  • Verificación de integridad legacy: Distribuciones Linux que aún
    proporcionan SHA1SUMS (aunque la mayoría ya migró a SHA256).
  • Git: SHA1 es el núcleo del sistema de objetos de Git. Una colisión
    SHA1 permitiría manipular el historial de un repositorio (ataque
    demostrado por Linus Torvalds como «ataque al Git»). El proyecto Git
    está migrando a SHA256.
  • Certificados SSL/TLS: Los certificados SHA1 fueron descontinuados
    por los navegadores en 2017. Hoy cualquier certificado SHA1 es
    considerado inválido.
  • Forense: Identificación de malware por SHA1 en bases como VirusTotal.

🧪 Código Python — SHA1

import hashlib
import hmac

# ─── Generar hash ─────────────────────────────────────────
def sha1_hash(data: str) -> str:
    return hashlib.sha1(data.encode()).hexdigest()

def sha1_file(path: str) -> str:
    h = hashlib.sha1()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

# ─── SHA1 con sal ─────────────────────────────────────────
def sha1_with_salt(password: str, salt: str) -> str:
    return hashlib.sha1((salt + password).encode()).hexdigest()

# ─── Verificar ────────────────────────────────────────────
def sha1_verify(data: str, expected_hash: str) -> bool:
    return hmac.compare_digest(sha1_hash(data), expected_hash)

# ─── Detectar formato ─────────────────────────────────────
import re

def es_sha1(cadena: str) -> bool:
    return bool(re.fullmatch(r'[0-9a-fA-F]{40}', cadena))

# ─── SHA1 de un commit de Git ─────────────────────────────
def sha1_git_object(tipo: str, contenido: bytes) -> str:
    """Calcula el hash Git de un objeto (blob, tree, commit, tag)."""
    raw = f"{tipo} {len(contenido)}\0".encode() + contenido
    return hashlib.sha1(raw).hexdigest()

# git hash-object == sha1_git_object("blob", b"hello world")

3. SHA256 (SHA-2)

Longitud: 256 bits (64 caracteres hex). Desarrollado por la NSA en 2001.
Es el estándar actual de hashing criptográfico. Resistente a colisiones
(ninguna conocida práctica).

Es el hash más usado hoy en día: certificados SSL/TLS, firmas de software,
blockchain (Bitcoin usa SHA256 doble), verificación de integridad de
descargas, y prácticamente cualquier aplicación moderna que necesite un hash.

Usos en CTF

  • Hash Length Extension: A pesar de ser más seguro que MD5/SHA1, SHA256
    también usa estructura Merkle-Damgård y es vulnerable a length extension.
  • Bitcoin/blockchain: Retos que involucran minado, prueba de trabajo,
    o direcciones de Bitcoin (SHA256 + RIPEMD160).
  • Firma de archivos: Verificar integridad de binarios en retos de
    forense o reversing.
  • Identificación: 64 caracteres hex. Modo hashcat: -m 1400.
  • Merkle tree: Retos que implementan árboles de Merkle para verificar
    integridad de datos distribuidos.
  • PBKDF2-HMAC-SHA256: Derivación de clave usada en algunos retos de
    criptografía.

Usos en el mundo real

  • Verificación de integridad de descargas: sha256sum es el comando
    estándar para verificar ISOs de Linux, instaladores, y cualquier archivo
    descargado. Kali Linux, Ubuntu, Debian todos proporcionan SHA256SUMS.
  • Blockchain: Bitcoin usa SHA256 doble (SHA256(SHA256(data))) como
    prueba de trabajo y para direcciones. Entender SHA256 es esencial para
    auditar criptomonedas.
  • Certificados SSL/TLS: Todos los certificados modernos usan SHA256
    como algoritmo de firma. Un certificado SHA256 es requisito para
    ser aceptado por navegadores actuales.
  • Firma de código: Binarios firmados con SHA256 (Authenticode,
    APK signing, etc.).
  • Integridad de contenedores: Docker usa SHA256 para identificar
    imágenes y capas.

🧪 Código Python — SHA256

import hashlib
import hmac

# ─── Generar hash ─────────────────────────────────────────
def sha256_hash(data: str) -> str:
    return hashlib.sha256(data.encode()).hexdigest()

def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

# ─── SHA256 doble (Bitcoin) ───────────────────────────────
def sha256_double(data: str) -> str:
    return sha256_hash(sha256_hash(data))

# ─── Verificar ────────────────────────────────────────────
def sha256_verify(data: str, expected_hash: str) -> bool:
    return hmac.compare_digest(sha256_hash(data), expected_hash)

# ─── Detectar formato ─────────────────────────────────────
import re

def es_sha256(cadena: str) -> bool:
    return bool(re.fullmatch(r'[0-9a-fA-F]{64}', cadena))

# ─── Iteraciones HMAC-SHA256 (PBKDF2-like simple) ────────
def pbkdf2_sha256_simple(password: str, salt: str, iterations: int = 100000) -> str:
    key = password.encode()
    for _ in range(iterations):
        key = hashlib.sha256(key + salt.encode()).digest()
    return key.hex()

# ─── Checksum de archivo (equivalente a sha256sum) ───────
def sha256sum(path: str) -> str:
    import os
    if not os.path.isfile(path):
        return None
    return f"{sha256_file(path)}  {os.path.basename(path)}"

4. SHA512 (SHA-2)

Longitud: 512 bits (128 caracteres hex). Parte de la misma familia SHA-2.
Más lento que SHA256 porque procesa bloques de 1024 bits vs 512 bits,
pero ofrece un espacio de salida mucho mayor.

Usado donde se requiere seguridad adicional contra ataques cuánticos
(las funciones hash son menos vulnerables a Grover que el cifrado simétrico,
pero el espacio de 512 bits ofrece más margen). También es común en
sistemas Unix/Linux para almacenar contraseñas ($6$).

Usos en CTF

  • Identificación: 128 caracteres hex. Menos común que SHA256, pero
    aparece en retos de criptografía avanzada.
  • Hashing de contraseñas Linux: Los hashes /etc/shadow con formato
    $6$salt$hash usan SHA512 (5000 rondas por defecto). Retos de
    escalada de privilegios o cracking.
  • Derivación de clave: Algunos protocolos custom usan SHA512 para
    generar claves más largas en una sola iteración.
  • Modo hashcat: -m 1700 para SHA512 plano, -m 1800 para SHA512
    crypt (Linux).

Usos en el mundo real

  • Contraseñas en Linux: El formato $6$ en /etc/shadow usa SHA512
    con 5000 rondas por defecto. Hardening de sistemas implica aumentar
    el número de rondas en /etc/login.defs.
  • Verificación de integridad: Distribuciones de alta seguridad que
    proporcionan checksums SHA512 además de SHA256.
  • DNSsec: Algoritmos de firma DNSSEC usan SHA512.
  • Derivación de clave: Donde se necesitan 512 bits de material de
    clave (ej: cifrado AES-256 + HMAC-SHA256 con dos claves derivadas
    del mismo material).

🧪 Código Python — SHA512

import hashlib
import hmac

# ─── Generar hash ─────────────────────────────────────────
def sha512_hash(data: str) -> str:
    return hashlib.sha512(data.encode()).hexdigest()

def sha512_file(path: str) -> str:
    h = hashlib.sha512()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

# ─── Verificar ────────────────────────────────────────────
def sha512_verify(data: str, expected_hash: str) -> bool:
    return hmac.compare_digest(sha512_hash(data), expected_hash)

# ─── Detectar formato ─────────────────────────────────────
import re

def es_sha512(cadena: str) -> bool:
    return bool(re.fullmatch(r'[0-9a-fA-F]{128}', cadena))

# ─── Simular hash estilo /etc/shadow $6$ ──────────────────
def sha512_crypt_simple(password: str, salt: str) -> str:
    """Versión simplificada del hash SHA512 crypt de Linux."""
    h = hashlib.sha512((salt + password).encode()).digest()
    for _ in range(4999):  # 5000 rondas total
        h = hashlib.sha512(h + password.encode()).digest()
    return f"$6${salt}${h.hex()}"

# ─── Parsear hash de /etc/shadow ─────────────────────────
def parse_shadow_hash(hash_str: str) -> dict:
    """Analiza un hash de /etc/shadow."""
    parts = hash_str.split("$")
    if len(parts) < 4:
        return {"error": "formato inválido"}
    return {
        "algoritmo": {"1": "MD5", "5": "SHA256", "6": "SHA512"}.get(parts[1], parts[1]),
        "salt": parts[2],
        "hash": parts[3],
    }

5. HMAC (Hash-based Message Authentication Code)

HMAC no es una función hash — es un MAC (Message Authentication Code)
construido a partir de una función hash y una clave secreta.

HMAC(K, m) = H((K XOR opad) || H((K XOR ipad) || m))

Provee integridad + autenticación: garantiza que el mensaje no fue
modificado y que fue generado por alguien que conoce la clave.

Usos en CTF

  • Hash Length Extension Attack: HMAC fue diseñado específicamente para
    resistir este ataque. Si un CTF usa H(secret || message) como MAC,
    es vulnerable. Si usa HMAC(secret, message), no lo es.
  • JWT con HMAC: Tokens JWT firmados con HS256 (HMAC-SHA256). Retos
    de secret débil, fuerza bruta del secret, o confusión RS256 → HS256.
  • API tokens: Retos donde un token HMAC debe ser forjado sin conocer
    la clave.
  • Timing attack: Comparación de HMAC. Usar hmac.compare_digest()
    previene timing attacks; usar == los permite.

Usos en el mundo real

  • APIs REST: Firmas HMAC para autenticar requests (AWS Signature V4,
    Stripe, Binance, etc.). El request incluye un HMAC de los parámetros
    usando una clave secreta compartida.
  • JWT (HS256): El algoritmo HS256 usa HMAC-SHA256 para firmar tokens.
    Es el más común en aplicaciones web.
  • OAuth 1.0: Las firmas HMAC son parte del protocolo.
  • Webhooks: Los webhooks de GitHub, Stripe, etc. se firman con HMAC
    para verificar que vienen del servicio legítimo.
  • Derivación de clave: PBKDF2 usa HMAC iterativamente.

🧪 Código Python — HMAC

import hmac
import hashlib
import base64

# ─── HMAC básico ──────────────────────────────────────────
def hmac_sha256(key: str, message: str) -> str:
    return hmac.new(key.encode(), message.encode(), hashlib.sha256).hexdigest()

def hmac_sha1(key: str, message: str) -> str:
    return hmac.new(key.encode(), message.encode(), hashlib.sha1).hexdigest()

def hmac_md5(key: str, message: str) -> str:
    return hmac.new(key.encode(), message.encode(), hashlib.md5).hexdigest()

# ─── HMAC en Base64 (JWT, APIs) ──────────────────────────
def hmac_b64(key: str, message: str) -> str:
    sig = hmac.new(key.encode(), message.encode(), hashlib.sha256).digest()
    return base64.urlsafe_b64encode(sig).rstrip(b"=").decode()

# ─── Verificar HMAC (con comparación segura) ─────────────
def hmac_verify(key: str, message: str, expected: str) -> bool:
    computed = hmac_sha256(key, message)
    return hmac.compare_digest(computed, expected)

# ─── Fuerza bruta de clave HMAC ──────────────────────────
def hmac_crack_key(message: str, target_hmac: str, wordlist: list) -> str:
    for word in wordlist:
        if hmac_sha256(word, message) == target_hmac:
            return word
    return None

# ─── Simular JWT HS256 ───────────────────────────────────
import json

def jwt_hs256(payload: dict, secret: str) -> str:
    header = base64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}))
    body = base64url_encode(json.dumps(payload))
    message = f"{header}.{body}"
    sig = hmac_b64(secret, message)
    return f"{message}.{sig}"

def base64url_encode(data: str) -> str:
    return base64.urlsafe_b64encode(data.encode()).decode().rstrip("=")

6. bcrypt

bcrypt es un algoritmo de hashing de contraseñas diseñado por Niels Provos
y David Mazières en 1999. Está específicamente diseñado para ser lento
y adaptativo: el factor de costo se puede incrementar a medida que
el hardware mejora.

A diferencia de MD5/SHA256, bcrypt incluye la sal automáticamente en el
output, y su lentitud (por diseño) hace que la fuerza bruta sea inviable
para contraseñas de complejidad media.

Usos en CTF

  • Identificación: El formato $2b$10$... con 60 caracteres es
    inconfundible. Identificar el tipo de hash es el primer paso.
  • Cracking con wordlists: bcrypt es el más lento de crackear.
    Un hashcat en una GPU hace ~300 hashes/segundo vs millones para MD5.
    Los CTFs suelen dar pistas sobre la contraseña para hacerlo viable.
  • Factor de costo: Algunos retos usan $2b$04$ (costo 4, muy bajo)
    para hacerlo crackeable. Otros usan $2b$12$ (estándar) o más.
  • Comparación de hashes: Entender que bcrypt incluye sal y costo en
    el propio hash (todo-en-uno) es clave.

Usos en el mundo real

  • Almacenamiento de contraseñas: Es el estándar recomendado por OWASP.
    Usado por Django (por defecto desde 1.4), Flask-Bcrypt, y muchas otras
    plataformas.
  • Migración desde MD5/SHA1: Proyectos que migran su almacenamiento de
    contraseñas a bcrypt por seguridad.
  • Hardening de autenticación: Incrementar el factor de costo según
    el hardware disponible. Costo 12 (~250ms) es el recomendado actualmente.
  • Defensa contra rainbow tables: Cada hash bcrypt incluye una sal
    única de 128 bits, haciendo las rainbow tables inviables.

🧪 Código Python — bcrypt

import bcrypt as _bcrypt  # pip install bcrypt

# ─── Hash de contraseña ───────────────────────────────────
def bcrypt_hash(password: str, rounds: int = 12) -> str:
    """Genera un hash bcrypt. El salt se genera automáticamente."""
    salt = _bcrypt.gensalt(rounds=rounds)
    return _bcrypt.hashpw(password.encode(), salt).decode()

# ─── Verificar contraseña ─────────────────────────────────
def bcrypt_verify(password: str, hash_str: str) -> bool:
    return _bcrypt.checkpw(password.encode(), hash_str.encode())

# ─── Extraer información del hash ─────────────────────────
def bcrypt_parse(hash_str: str) -> dict:
    """Analiza un hash bcrypt y extrae sus componentes."""
    parts = hash_str.split("$")
    if len(parts) != 4 or not parts[1].startswith("2"):
        return {"error": "formato bcrypt inválido"}
    return {
        "version": parts[1],
        "costo": int(parts[2]),
        "salt": parts[3][:22],
        "hash": parts[3][22:],
    }

# ─── Estimar tiempo de cracking ───────────────────────────
def bcrypt_estimate_strength(hash_str: str, wordlist_size: int = 1000000) -> str:
    """Estima el tiempo de fuerza bruta para este hash."""
    info = bcrypt_parse(hash_str)
    if "error" in info:
        return "hash inválido"
    costo = info["costo"]
    # Aprox: 2^costo * ~1ms por hash en CPU
    tiempo_por_hash_ms = 2 ** (costo - 10)  # normalizado a costo 10 ≈ 5ms
    tiempo_total = (wordlist_size * tiempo_por_hash_ms) / 1000  # segundos
    if tiempo_total < 60:
        return f"{tiempo_total:.0f} segundos"
    if tiempo_total < 3600:
        return f"{tiempo_total/60:.0f} minutos"
    return f"{tiempo_total/3600:.1f} horas"

# ─── Bruteforce limitado (solo para CTFs con pistas) ─────
def bcrypt_crack(hash_str: str, wordlist: list) -> str:
    for word in wordlist:
        if _bcrypt.checkpw(word.encode(), hash_str.encode()):
            return word
    return None

7. scrypt

scrypt es un algoritmo de derivación de clave diseñado por Colin Percival
en 2009 (para Tarsnap). Es intencionalmente lento y consume mucha memoria (memory-hard), lo que lo hace resistente a ataques con hardware
especializado (FPGAs, ASICs).

Mientras que bcrypt solo es caro en CPU, scrypt también es caro en memoria:
un atacante necesita mucha RAM para paralelizar el ataque.

Usos en CTF

  • Identificación: Formato $7$ (no confundir con SHA512 $6$).
  • Factor de memoria: El parámetro N (coste de CPU/memoria), r (tamaño
    de bloque), p (paralelismo). Retos con parámetros débiles para hacer
    el hash crackeable.
  • Derivación de clave: Algunos CTFs piden derivar una clave de
    cifrado desde una contraseña usando scrypt.
  • Cryptocurrency: Litecoin, Dogecoin y otras usan scrypt como prueba
    de trabajo. Retos de minado o análisis de blockchain.

Usos en el mundo real

  • Criptomonedas: Litecoin (LTC), Dogecoin (DOGE) y otras usan scrypt
    como algoritmo de prueba de trabajo (minado).
  • Derivación de clave: Convertir contraseñas en claves de cifrado
    para discos encriptados (LUKS usa scrypt desde 2018 o PBKDF2).
  • Almacenamiento de contraseñas: Menos común que bcrypt en apps web,
    pero usado en sistemas más security-conscious.
  • Hardening: Aumentar los parámetros N, r, p para proteger contra
    hardware especializado.

🧪 Código Python — scrypt

import hashlib
import base64

# ─── scrypt básico (usando hashlib - Python 3.6+) ────────
def scrypt_hash(password: str, salt: str = None, N: int = 16384,
                r: int = 8, p: int = 1, dklen: int = 64) -> str:
    """Deriva clave usando scrypt. salt se genera si no se provee."""
    import os
    if salt is None:
        salt = base64.b64encode(os.urandom(16)).decode()
    key = hashlib.scrypt(
        password.encode(),
        salt=salt.encode() if isinstance(salt, str) else salt,
        n=N, r=r, p=p, dklen=dklen
    )
    return f"$7$N={N},r={r},p={p}${salt}${key.hex()}"

# ─── Verificar contraseña contra hash scrypt ─────────────
def scrypt_verify(password: str, hash_str: str) -> bool:
    """Verifica una contraseña contra un hash scrypt generado por scrypt_hash()."""
    parts = hash_str.split("$")
    if len(parts) != 4:
        return False
    params_str = parts[1]
    salt = parts[2]
    expected_hex = parts[3]

    # Parsear parámetros
    import re
    params = {}
    for p in params_str.split(","):
        k, v = p.split("=")
        params[k.strip()] = int(v.strip())

    computed = hashlib.scrypt(
        password.encode(),
        salt=salt.encode(),
        n=params.get("N", 16384),
        r=params.get("r", 8),
        p=params.get("p", 1),
        dklen=len(bytes.fromhex(expected_hex))
    )
    return hmac.compare_digest(computed.hex(), expected_hex)

# ─── scrypt con parámetros débiles (CTF) ─────────────────
def scrypt_ctf_weak(password: str, salt: str) -> str:
    """scrypt con parámetros débiles para CTFs (crackeable)."""
    key = hashlib.scrypt(
        password.encode(), salt=salt.encode(),
        n=2, r=1, p=1, dklen=32  # N=2 es extremadamente débil
    )
    return f"$7$N=2,r=1,p=1${salt}${key.hex()}"

# ─── Identificar formato scrypt ──────────────────────────
def es_scrypt(hash_str: str) -> bool:
    return hash_str.startswith("$7$")

8. Argon2

Argon2 es el ganador del Password Hashing Competition (PHC) en 2015.
Diseñado por Alex Biryukov, Daniel Dinu y Dmitry Khovratovich. Es el
estado del arte en hashing de contraseñas.

Tres variantes:

  • Argon2d: Resistente a timing attacks (usa acceso a memoria dependiente de datos)
  • Argon2i: Resistente a side-channel attacks (usa acceso a memoria independiente)
  • Argon2id: Híbrido (recomendado por defecto)

Actualmente recomendado por OWASP como el estándar para almacenamiento
de contraseñas.

Usos en CTF

  • Identificación: Formato $argon2id$v=19$m=65536,t=3,p=4$....
    Cada vez más común en CTFs modernos.
  • Parámetros débiles: Valores bajos de m (memoria), t (tiempo),
    p (paralelismo) hacen el hash crackeable.
  • Derivación de clave: Argon2 puede derivar claves de cualquier
    longitud desde una contraseña.
  • Cracking: Es el más lento de los algoritmos de password. Un hashcat
    con GPU hace pocos hashes/segundo. Los CTFs con Argon2 suelen dar
    pistas o usar parámetros muy reducidos.

Usos en el mundo real

  • Estándar OWASP: Recomendado para nuevo desarrollo. Si estás
    empezando un proyecto hoy, usa Argon2id.
  • PHP 7.2+: password_hash() con PASSWORD_ARGON2ID disponible
    desde PHP 7.3.
  • Python: hashlib.scrypt() y argon2-cffi (pip).
  • Key Derivation Function: Derivar claves de cifrado desde
    contraseñas de usuario.
  • Hardening: Parámetros recomendados: m=19456 (19MB), t=2, p=1.

🧪 Código Python — Argon2

# Requiere: pip install argon2-cffi
from argon2 import PasswordHasher, exceptions
from argon2 import Type as Argon2Type

# ─── Hash con Argon2id (recomendado) ──────────────────────
def argon2_hash(password: str) -> str:
    """Genera hash Argon2id con parámetros seguros por defecto."""
    ph = PasswordHasher(
        time_cost=2,        # t = 2 iteraciones
        memory_cost=19456,  # m = 19 MB
        parallelism=1,      # p = 1 hilo
        hash_len=32,        # 256 bits de salida
        type=Argon2Type.ID  # Argon2id (recomendado)
    )
    return ph.hash(password)

# ─── Verificar ────────────────────────────────────────────
def argon2_verify(password: str, hash_str: str) -> bool:
    """Verifica contraseña contra hash Argon2."""
    ph = PasswordHasher()
    try:
        return ph.verify(hash_str, password)
    except exceptions.VerifyMismatchError:
        return False

# ─── Hash con parámetros débiles (CTF) ───────────────────
def argon2_ctf_weak(password: str) -> str:
    """Argon2 con parámetros mínimos para que sea crackeable."""
    ph = PasswordHasher(
        time_cost=1,
        memory_cost=8,  # 8 KB (mínimo)
        parallelism=1,
        hash_len=16,
        type=Argon2Type.I
    )
    return ph.hash(password)

# ─── Parsear hash ─────────────────────────────────────────
def argon2_parse(hash_str: str) -> dict:
    """Extrae parámetros de un hash Argon2."""
    parts = hash_str.split("$")
    if len(parts) < 5 or not parts[1].startswith("argon2"):
        return {"error": "formato Argon2 inválido"}

    info = {
        "type": parts[1],
        "version": parts[2].lstrip("v="),
    }
    for param in parts[3].split(","):
        k, v = param.split("=")
        info[k] = int(v)
    return info

9. NTLM (Windows NT LAN Manager)

NTLM no es una función hash tradicional — es un protocolo de autenticación
de Microsoft. El «hash NTLM» es en realidad el resultado de aplicar
MD4 (tres veces a la contraseña en Unicode).

Formato: 32 caracteres hex (como MD5, pero usa MD4 internamente).

A diferencia de los hashes de Unix, NTLM no usa sal. Esto significa
que dos usuarios con la misma contraseña tienen exactamente el mismo hash
NTLM, y las rainbow tables son efectivas.

Usos en CTF

  • Pass-the-Hash: El ataque más famoso de Windows. Obtienes el hash
    NTLM de un usuario y lo usas directamente para autenticarte sin
    necesidad de conocer la contraseña en texto plano.
  • Identificación: 32 caracteres hex (como MD5). Modo hashcat -m 1000.
  • Dump de SAM: Retos donde extraes hashes del archivo SAM de Windows
    y necesitas crackearlos.
  • Mimikatz: Retos que simulan el volcado de credenciales con Mimikatz.
  • Kerberos: Hashes NTLM también se usan para generar tickets Kerberos.

Usos en el mundo real

  • Ataques Pass-the-Hash: Es la técnica más usada en post-explotación
    de entornos Windows. Una vez que obtienes un hash NTLM (de SAM, LSASS,
    o NTDS.dit), puedes moverte lateralmente sin crackear la contraseña.
  • Active Directory: Los hashes NTLM se almacenan en NTDS.dit (el
    controlador de dominio). Un atacante que extrae NTDS.dit obtiene todos
    los hashes del dominio.
  • Responder / SMB Relay: Herramientas como Responder capturan hashes
    NTLM en la red para relay o cracking.
  • Cracking: Sin sal, los hashes NTLM se crackean rápido con hashcat
    (modo -m 1000). Una GPU moderna hace miles de millones de intentos
    por segundo.

🧪 Código Python — NTLM

import hashlib
import hmac
import binascii

# ─── Hash NTLM (MD4 de Unicode little-endian) ─────────────
def ntlm_hash(password: str) -> str:
    """Calcula el hash NTLM de una contraseña."""
    # La contraseña se convierte a UTF-16LE (little-endian)
    password_utf16 = password.encode("utf-16le")
    # MD4 (disponible en hashlib en algunos builds, o vía Cryptodome)
    try:
        md4 = hashlib.new("md4", password_utf16)
        return md4.hexdigest().upper()
    except ValueError:
        # Fallback: usar Cryptodome si hashlib no tiene md4
        from Cryptodome.Hash import MD4
        md4 = MD4.new(password_utf16)
        return md4.hexdigest().upper()

# ─── Hash NTLMv2 (más seguro que NTLMv1) ─────────────────
def ntlmv2_hash(username: str, password: str, domain: str = "") -> str:
    """NTLMv2 combina usuario+dominio con el hash NTLM."""
    ntlm = ntlm_hash(password)
    identity = (username.upper() + domain).encode("utf-16le")
    h = hmac.new(binascii.unhexlify(ntlm), identity, hashlib.md5)
    return h.hexdigest().upper()

# ─── Detectar formato ─────────────────────────────────────
import re

def es_ntlm(cadena: str) -> bool:
    """NTLM tiene 32 hex, como MD5. Pero suele estar en mayúsculas."""
    return bool(re.fullmatch(r'[0-9A-Fa-f]{32}', cadena))

# ─── Pass-the-Hash simulado ───────────────────────────────
def pass_the_hash(ntlm_hash_str: str, target: str) -> str:
    """
    Simula pass-the-hash. En la práctica usarías herramientas como
    impacket o mimikatz. Esto solo muestra el concepto.
    """
    return f"[*] Autenticándose contra {target} con hash {ntlm_hash_str}"

# ─── Cracking NTLM con wordlist ───────────────────────────
def ntlm_crack(hash_obj: str, wordlist: list) -> str:
    for word in wordlist:
        if ntlm_hash(word) == hash_obj:
            return word
    return None

10. Identificador Universal de Hashes

A diferencia del encoding (donde cada formato es visualmente distinto),
los hashes son difíciles de distinguir solo por su longitud. MD5 y NTLM
son ambos 32 hex. SHA256 y SHA3-256 son ambos 64 hex.

Esta herramienta intenta identificar cualquier hash usando longitud,
formato, prefijos, y contenido.
🧪 Código Python — Identificador de hashes + Cracking

import re
import hashlib

# ─── Base de datos de tipos de hash ───────────────────────
HASH_PATTERNS = [
    # (nombre, regex, longitud_hex, prefijo, modo_hashcat)
    ("MD5",          r'^[0-9a-fA-F]{32}$', 32,   None,    "0"),
    ("NTLM",         r'^[0-9a-fA-F]{32}$', 32,   None,    "1000"),
    ("SHA1",         r'^[0-9a-fA-F]{40}$', 40,   None,    "100"),
    ("SHA256",       r'^[0-9a-fA-F]{64}$', 64,   None,    "1400"),
    ("SHA512",       r'^[0-9a-fA-F]{128}$',128,  None,    "1700"),
    ("bcrypt",       r'^\$2[aby]\$\d{2}\$[A-Za-z0-9./]{53}$', None, "$2", "3200"),
    ("bcrypt-old",   r'^\$2[aby]\$\d{2}\$[A-Za-z0-9./]{53}$', None, "$2", "3200"),
    ("scrypt",       r'^\$7\$.*',            None, "$7",   "8900"),
    ("Argon2i",      r'^\$argon2i\$.*',      None, "$argon2i", "9200"),
    ("Argon2id",     r'^\$argon2id\$.*',     None, "$argon2id", "9200"),
    ("SHA256-crypt", r'^\$5\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$5", "7400"),
    ("SHA512-crypt", r'^\$6\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$6", "1800"),
    ("MD5-crypt",    r'^\$1\$[A-Za-z0-9./]+\$[A-Za-z0-9./]+$', None, "$1", "500"),
    ("SHA3-256",     r'^[0-9a-fA-F]{64}$', 64,   None,    "17300"),
    ("SHA3-512",     r'^[0-9a-fA-F]{128}$',128,  None,    "17400"),
    ("RIPEMD160",    r'^[0-9a-fA-F]{40}$', 40,   None,    "6000"),
    ("Whirlpool",    r'^[0-9a-fA-F]{128}$',128,  None,    "6100"),
    ("MySQL",        r'^[0-9a-fA-F]{16}$', 16,   None,    "200"),
    ("MySQL5",       r'^\*[0-9a-fA-F]{40}$', 40, "*",    "300"),
    ("CRC32",        r'^[0-9a-fA-F]{8}$',  8,    None,    "11500"),
    ("LM Hash",      r'^[0-9a-fA-F]{32}$', 32,   None,    "3000"),
    ("HalfLMCHalLM", r'^[0-9a-fA-F]{16}$', 16,   None,    "1500"),
]

def identificar_hash(cadena: str) -> list:
    """Identifica posibles tipos para un hash dado."""
    cadena = cadena.strip()
    resultados = []

    for nombre, regex, _, prefijo, modo_hc in HASH_PATTERNS:
        if prefijo and not cadena.startswith(prefijo):
            continue
        if regex and re.fullmatch(regex, cadena, re.IGNORECASE):
            resultados.append({
                "tipo": nombre,
                "hashcat_mode": modo_hc,
                "longitud": len(cadena),
            })

    if not resultados:
        return [{"tipo": "desconocido", "hashcat_mode": "?", "longitud": len(cadena)}]

    return resultados

# ─── Analizador completo ──────────────────────────────────
def analizar_hash(hash_str: str) -> dict:
    """Analiza un hash y devuelve información detallada."""
    return {
        "hash": hash_str,
        "longitud": len(hash_str),
        "caracteres": set(hash_str),
        "tiene_prefijo": any(hash_str.startswith(p) for _, _, _, p, _ in HASH_PATTERNS if p),
        "posibles_tipos": identificar_hash(hash_str),
        "recomendacion_hashcat": identificar_hash(hash_str)[0]["hashcat_mode"]
        if identificar_hash(hash_str) else "?",
    }

# ─── Cracking automático (intenta según tipo) ────────────
def crack_hash(hash_str: str, wordlist: list) -> str:
    """Intenta crackear un hash identificando primero su tipo."""
    tipos = identificar_hash(hash_str)
    if not tipos:
        return None

    tipo = tipos[0]["tipo"]

    for word in wordlist:
        word = word.strip()
        if tipo == "MD5" or tipo == "NTLM":
            try:
                if hashlib.md5(word.encode()).hexdigest().upper() == hash_str.upper():
                    return word
            except: pass
        if tipo == "SHA1":
            if hashlib.sha1(word.encode()).hexdigest() == hash_str:
                return word
        if tipo == "SHA256":
            if hashlib.sha256(word.encode()).hexdigest() == hash_str:
                return word
        if tipo == "SHA512":
            if hashlib.sha512(word.encode()).hexdigest() == hash_str:
                return word

    return None

# ─── Detección de hash débil por longitud ─────────────────
def detectar_hash_debil(hash_str: str) -> bool:
    """Detecta si un hash tiene una longitud que indica debilidad."""
    longitudes_debiles = {8: "CRC32 / LM Half",
                          16: "MySQL pre-5.7 / HalfLM",
                          32: "MD5 / NTLM (débiles)"}
    return longitudes_debiles.get(len(hash_str), None)

Bonus: Comparación de velocidad de cracking

Esta tabla muestra cuántos hashes por segundo puede probar una GPU
moderna (RTX 4090) para cada algoritmo, y cuánto tarda en crackear
una contraseña de 8 caracteres alfanuméricos (~2.8 billones de combinaciones):

AlgoritmoHash/s (RTX 4090)Tiempo para 8 chars alfanum
NTLM~200 B/s< 1 segundo
MD5~150 B/s< 1 segundo
SHA1~50 B/s~1 minuto
SHA256~20 B/s~2 minutos
SHA512~7 B/s~6 minutos
bcrypt (costo 5)~200 K/s~160 días
bcrypt (costo 10)~10 K/s~9 años
bcrypt (costo 12)~2.5 K/s~35 años
scrypt~500 /s~180 años
Argon2id~100 /s~900 años

Nota: Los valores son aproximados y dependen de la implementación, configuración de memoria (scrypt/Argon2) y velocidad de la GPU.

🧪 Código Python — Benchmark de velocidad

import time
import hashlib

def benchmark_hash(name: str, func, iterations: int = 10000) -> float:
    """Mide hashes por segundo de una función hash."""
    data = "test_password_123"
    start = time.perf_counter()
    for _ in range(iterations):
        func(data)
    elapsed = time.perf_counter() - start
    hashes_per_sec = iterations / elapsed
    print(f"{name:15s} → {hashes_per_sec:>12.0f} hash/s")
    return hashes_per_sec

# benchmark_hash("MD5", lambda d: hashlib.md5(d.encode()).hexdigest())
# benchmark_hash("SHA256", lambda d: hashlib.sha256(d.encode()).hexdigest())
# benchmark_hash("SHA512", lambda d: hashlib.sha512(d.encode()).hexdigest())

Tabla comparativa

#AlgoritmoBitsHex charsColisiones conocidasUso principalHashcat mode
1MD512832Sí (prácticas)Legacy, checksums0
2SHA116040Sí (prácticas)Legacy, Git100
3SHA25625664NoEstándar actual1400
4SHA512512128NoAlta seguridad, Linux1700
5HMACvariablevariableAutenticación de APIs
6bcrypt19260NoContraseñas (estándar)3200
7scryptvariablevariableNoContraseñas (memory-hard)8900
8Argon2variablevariableNoContraseñas (PHC winner)9200
9NTLM12832Sí (rainbow tables)Windows auth1000
10IdentificadorPentesting

Conclusión

El hashing es la base de la integridad digital y la autenticación moderna.
Saber identificar qué tipo de hash tienes delante, entender sus fortalezas
y debilidades, y conocer las herramientas para atacarlo o defenderlo,
es una habilidad fundamental en seguridad informática.

Regla de oro:

  • ¿Necesitas verificar integridad? → SHA256
  • ¿Necesitas almacenar contraseñas? → Argon2id o bcrypt
  • ¿Estás en un CTF? → Identifica el tipo, elige el modo hashcat correcto
  • ¿Ves un hash de 32 hex sin prefijo? → Podría ser MD5 o NTLM. No los confundas.

Este artículo es el segundo de una trilogía. El primero cubrió encoding.
Próximo: Encriptación — cifrado simétrico y asimétrico en CTFs y en el mundo real.