El talón de Aquiles de la criptografía: cómo los PRNG pueden arruinar tu seguridad

El talón de Aquiles de la criptografía: cómo los PRNG pueden arruinar tu seguridad

Celia Catalán



A02:2021 - Cryptographic failures – Números aleatorios pseudoaleatorios (PRNG)

Introducción

La generación de números aleatorios en programación es crucial para muchas aplicaciones, especialmente en criptografía. Un número aleatorio es aquel que no puede ser predicho o anticipado, lo que garantiza la seguridad en la generación de claves, tokens, o cualquier valor sensible que deba permanecer secreto.

Existen dos tipos principales de generadores de números aleatorios (RNG, por sus siglas en inglés):

1. Generadores de números aleatorios pseudoaleatorios (PRNG): Utilizan algoritmos deterministas que, aunque producen secuencias de números que parecen aleatorios, son predecibles si se conoce el estado inicial o semilla. Esto es peligroso en contextos criptográficos, ya que un atacante podría replicar la secuencia y anticipar valores sensibles.

2. Generadores de números aleatorios verdaderamente aleatorios (TRNG): Basados en fenómenos físicos impredecibles, como la radiación electromagnética o el ruido térmico, estos generadores son mucho más seguros, ya que no son predecibles.

La vulnerabilidad surge cuando se utilizan PRNG inseguros o mal implementados en sistemas criptográficos. Si un atacante puede predecir los números generados, puede romper la seguridad de los sistemas, derivar claves criptográficas, interceptar comunicaciones seguras o acceder a recursos protegidos.

Esta relación entre aleatoriedad y seguridad es una de las razones por las que OWASP incluye los fallos en la criptografía, como la generación de números aleatorios inseguros, entre las principales preocupaciones de seguridad (A02:2021 Cryptographic Failures).

Introducción Técnica

Un algoritmo de Generación de Números Pseudoaleatorios (PRNG) es un sistema determinista que transforma un valor inicial conocido como semilla mediante una función matemática predefinida para producir una secuencia de números que, en apariencia, parecen aleatorios. Sin embargo, esta aleatoriedad es solo superficial, ya que la secuencia está completamente determinada por la semilla inicial. Esto implica que dos instancias del mismo algoritmo, si son inicializadas con la misma semilla, generarán una secuencia idéntica de números. Aunque esta propiedad puede ser útil en ciertos contextos, como en simulaciones reproducibles, es altamente problemática en criptografía, donde la predictibilidad socava los principios fundamentales de seguridad.

Desde el punto de vista de la seguridad criptográfica, se depende en gran medida de la calidad y la imprevisibilidad de los números aleatorios utilizados en procesos como la generación de claves, vectores de inicialización y nonce. Los PRNG tradicionales, como los generadores congruenciales lineales (LCG), son vulnerables a ataques debido a su estructura matemática simple y la poca entropía utilizada en las semillas. Estos generadores suelen depender de valores predecibles, como timestamps, que un atacante puede inferir.

A diferencia de los PRNG tradicionales, los generadores de números pseudoaleatorios criptográficamente seguros (CSPRNG) están diseñados para resistir ataques incluso si se expone parcialmente su estado interno o si se interceptan múltiples salidas de la secuencia. Los CSPRNG utilizan fuentes de entropía más robustas, puede ser un generador de números pseudoaleatorios en hardware o incluso procesos de sistemas impredecibles, y aplican algoritmos criptográficos avanzados, como los basados en cifrados en bloque o funciones hash, para garantizar la imprevisibilidad de la secuencia.

Vulnerabilidades principales

Predictibilidad en generación de claves criptográficas: La predictibilidad de los PRNG inseguros compromete la generación de claves criptográficas. Si un atacante logra inferir la semilla utilizada S_0, ya sea mediante un análisis de las salidas del generador o adivinando una semilla basada en un valor predecible como un timestamp, puede reconstruir la clave privada asociada y descifrar mensajes o realizar firmas fraudulentas. Este problema ha afectado históricamente a algoritmos como el RSA y el ECC, donde la calidad de las claves depende directamente de la calidad de los números aleatorios.

Nonce reutilizados o predecibles: En sistemas criptográficos como AES-GCM o ChaCha20-Poly1305, la reutilización de un mismo nonce (número único por mensaje) compromete la confidencialidad e integridad de los datos. Si un PRNG genera nonces N predecibles o reutiliza valores debido a colisiones en un espacio reducido, un atacante puede realizar ataques como la recuperación de texto plano a través de análisis de diferencias. Un caso notable es la reutilización de nonces en la implementación de WPA2, que permitió ataques como KRACK.

Sesiones JWT vulnerables: Los tokens JSON Web Tokens (JWT) dependen de valores únicos y aleatorios para evitar que sean replicados o adivinados. Si un PRNG inseguro genera identificadores de sesión o valores de firma predecibles, un atacante podría forjar tokens válidos para acceder a sistemas protegidos. Por ejemplo, una firma HMAC basada en claves derivadas de números pseudoaleatorios predecibles podría permitir un ataque de suplantación con esfuerzo computacional reducido.

Problemas en protocolos de handshake: Protocolos como TLS y SSH dependen de valores aleatorios para asegurar que las sesiones sean únicas y no puedan ser reproducidas por un atacante. Si los números pseudoaleatorios utilizados en el handshake son predecibles, un atacante podría ejecutar ataques de "replay" o forzar la renegociación de claves débiles, comprometiendo la confidencialidad del canal. Esto se evidenció en versiones antiguas de SSL/TLS que utilizaban PRNG deficientes.

Comparación de generación segura vs. insegura

La generación se puede dividir en tres pasos principales:

1. Obtención de la semilla inicial:

En la generación insegura, la semilla suele derivarse de fuentes fácilmente predecibles, como un timestamp, el identificador de proceso (PID) o incluso un valor constante. Por ejemplo, un generador congruencial lineal puede usar la hora actual en segundos, lo que reduce drásticamente el espacio de búsqueda para un atacante. Por otro lado, en la generación segura, la semilla proviene de una fuente de entropía robusta, como estados internos recolectados por el sistema operativo. Esto asegura que la semilla no sea predecible ni reproducible.

2. Cálculo del estado interno:

Un PRNG inseguro transforma la semilla inicial mediante operaciones matemáticas simples, como multiplicaciones o sumas modulares, que, aunque generan números aparentemente aleatorios, presentan patrones detectables con suficiente análisis. En cambio, un generador seguro aplica algoritmos criptográficos como funciones hash o cifrados en bloque, los cuales poseen una gran resistencia a la reversión del estado interno.

3. Producción de la secuencia de números:

Los PRNG inseguros generan cada número en la secuencia mediante fórmulas rápidas pero deterministas, lo que permite predecir futuras salidas si se conoce el estado actual. Por ejemplo, al observar algunas salidas consecutivas, un atacante podría deducir el algoritmo y predecir las siguientes (en casos extremadamente sencillos). En comparación, los generadores seguros producen números utilizando transformaciones que dependen tanto del estado interno como de la entropía adicional recolectada durante la operación, haciendo que cada salida sea prácticamente impredecible.

Vamos al código...


Terminal

               
#include (stdio .h="")
#include (stdlib .h="")
#include (time .h="")

int generate_random_number() {
    static int initialized = 0;
       if (!initialized) {
        srand((unsigned int)time(NULL)); // (-- Uso del timestamp como semilla
        initialized = 1;
    }

    return rand() % 100;
}

int main() {
    printf(Generando números pseudoaleatorios:\n);
    for (int i = 0; i (5,i++) {
        printf(%d\n, generate_random_number());
    }
    return 0;
}

      
Inicialmente se hace uso de time(NULL) como semilla, mediante la función srand se inicializa el generador de números pseudoaleatorios con el valor actual del timestamp (segundos desde epoch). A pesar de que de primeras puede parecer razonable, dado que el tiempo cambia constantemente, no deja de ser predecible. Si un atacante consigue conocer el momento en que se ejecuta el programa, puede calcular fácilmente la semilla utilizada.

srand() inicializa el generador de número aleatorios creando una semilla, en cambio rand() simplemente devuelve un número aleatorio, este último utiliza un generador congruencial lineal (LCG), siendo matemáticamente simple y produciendo secuencias predecibles.

El generador inicializa la semilla una única vez, nunca es actualizada con nueva entropía, lo que implica que todas las ejecuciones del programa, en un periodo de corto, generarán las mismas secuencias de números pseudoaleatorios, resultando en colisiones previsibles.

A mayores, el espacio de salida es limitado. El rango de salida de rand() es pequeño, típicamente entre 0 y 2^32-1, y en el caso del ejemplo, se reduce con el operador módulo. Facilitando la posibilidad de ataques de fuerza bruta.

Por ello se propone la siguiente aproximación para generar los mismos posibles valores, de una forma segura.
  • Uso de syscall como getrandom(), la cual es una fuente de entropía criptográficamente segura proporcionada por Linux (requiere glibc versión 2.25 o superior).
  • Si el sistema no soporta getrandom(), se puede usar /dev/urandom para obtener números aleatorios de calidad similar. En sistemas Windows la función equivanete sería BCryptGenRandom.
  • Alternativamente, en CPUs con RDRAND (Intel/AMD), se puede hacer uso de _rdrand32_step, permitiendo obtener números directamente del hardware.

Terminal

#include (fcntl.h)
#include (errno.h)
#include (string.h)
#include (linux/random.h)
#include (sys/syscall.h)

int generate_secure_random_number() {
    uint32_t random_number;

    // Usar syscall getrandom para obtener un número aleatorio seguro
    if (syscall(SYS_getrandom, random_number, sizeof(random_number), 0) == -1) {
        perror("Error al generar un número aleatorio seguro");
        exit(EXIT_FAILURE);
    }

    return random_number % 100;
}

int main() {
    printf("Generando números aleatorios seguros:\n");
    for (int i = 0; i ( 5; i++) {
        printf(%d\n, generate_secure_random_number());
    }
    return 0;
}
    
      

Daniel Puente, pentester en Zerolynx
Regresar al blog

Deja un comentario

Ten en cuenta que los comentarios deben aprobarse antes de que se publiquen.