Vorhersagbarer Seed in Pseudo-Zufallszahlengenerator (PRNG)

Beschreibung

Vorhersagbarer Seed in Pseudo-Zufallszahlengenerator (PRNG) ist eine Schwachstelle, die auftritt, wenn ein PRNG mit einem Seed-Wert initialisiert wird, den ein Angreifer vorhersagen oder bestimmen kann. Häufige vorhersagbare Seed-Quellen umfassen Systemzeit (oft mit Sekundenpräzision), Prozess-IDs, Thread-IDs, Speicheradressen oder Kombinationen dieser Werte. Da PRNGs deterministische Ausgabesequenzen basierend auf ihrem Seed erzeugen, reduziert ein vorhersagbarer Seed dramatisch den Suchraum, den ein Angreifer durchsuchen muss, um generierte Werte vorherzusagen. Anstatt eine astronomische Anzahl möglicher Zufallssequenzen zu durchsuchen, müssen Angreifer nur die relativ kleine Menge möglicher Seed-Werte berücksichtigen.

Risiko

Vorhersagbare Seeds verwandeln theoretische Sicherheit in praktische Schwachstellen. Systemzeit-Seeds reduzieren typischerweise den Seed-Raum auf tausende oder Millionen von Werten (Sekunden oder Millisekunden in einem Zeitfenster), verglichen mit den theoretisch möglichen 2^128 oder mehr Werten. Prozess-IDs sind typischerweise 15-16 Bits (32.768 bis 65.536 Werte). Wenn Angreifer abschätzen können, wann ein Seed generiert wurde (Anwendungsstart, Session-Erstellung usw.), können sie alle möglichen Seeds in Sekunden oder Minuten aufzählen. Reale Exploits haben vorhersagbare Seeds verwendet, um Kryptowährungs-Wallets zu kompromittieren (CVE-2020-7010), Session-Tokens vorherzusagen, Authentifizierungscodes zu fälschen und Verschlüsselung zu brechen. Der Debian-OpenSSL-Bug (CVE-2008-0166), bei dem nur die Prozess-ID für das Seeding verwendet wurde, betraf tausende Systeme und bleibt eine der schwerwiegendsten kryptografischen Schwachstellen der Geschichte.

Lösung

Speisen Sie PRNGs ausschließlich aus kryptografisch sicheren, unvorhersagbaren Entropiequellen. Verwenden Sie vom Betriebssystem bereitgestellte CSPRNGs (/dev/urandom, CryptGenRandom, SecureRandom.getInstanceStrong()), die Entropie aus Hardware-Quellen und Systemereignissen sammeln. Leiten Sie Seeds niemals aus Systemzeit, Prozess-IDs, Speicheradressen oder anderen vorhersagbaren Systemzuständen ab. Wenn Sie aus Leistungsgründen einen statistischen PRNG verwenden müssen, seeden Sie ihn aus einem CSPRNG mit mindestens 256 Bits Entropie. Implementieren Sie Defense-in-Depth, indem Sie mehrere unabhängige Entropiequellen kombinieren, wenn verfügbar. Erwägen Sie Hardware-Zufallszahlengeneratoren für Hochsicherheitsanwendungen. Testen Sie auf vorhersagbares Seeding durch Auditing des Initialisierungscodes und Verwendung statischer Analysewerkzeuge.

Häufige Auswirkungen

AuswirkungDetails
ZugriffskontrolleUmfang: Zugriffskontrolle

Angreifer können Authentifizierungs-Tokens, Session-IDs und Passwörter vorhersagen, die mit vorhersagbar geseedeten PRNGs generiert wurden, und so Authentifizierung umgehen und unbefugten Zugriff erlangen.
VertraulichkeitUmfang: Vertraulichkeit

Kryptografische Schlüssel, die aus vorhersagbaren Seeds generiert wurden, können durch Seed-Aufzählung rekonstruiert werden und so verschlüsselte Daten kompromittieren.
IntegritätUmfang: Integrität

CSRF-Tokens, Nonces und andere Integritätsmechanismen werden vorhersagbar und fälschbar, wenn sie auf vorhersagbar geseedeten PRNGs basieren.

Beispielcode

Anfälliger Code (Python/Java)

Die folgenden Beispiele demonstrieren Schwachstellen durch vorhersagbare Seeds:

# Anfällig: Vorhersagbare PRNG-Seeds
import random
import time
import os

# Anfällig: Aktuelle Zeit als Seed
def anfällig_zeit_seed():
    # Anfällig: time.time() ist vorhersagbar
    # Angreifer, der ungefähre Zeit kennt, grenzt auf tausende Werte ein
    random.seed(time.time())
    return random.getrandbits(128)

# Anfällig: Prozess-ID als Seed
def anfällig_pid_seed():
    # Anfällig: PID ist nur 15-16 Bits
    # In Millisekunden aufzählbar
    random.seed(os.getpid())
    return random.getrandbits(64)

# Anfällig: Zeitstempel aus Anfrage
def anfällig_anfrage_zeit(request_timestamp):
    # Anfällig: Angreifer kontrolliert oder kennt Zeitstempel
    random.seed(int(request_timestamp))
    return format(random.getrandbits(64), '016x')

# Anfällig: Mikrosekunden-Zeit (etwas besser, aber immer noch schwach)
def anfällig_mikrosekunden_seed():
    # Anfällig: Immer noch nur ~20 Bits Entropie pro Sekunde
    random.seed(int(time.time() * 1000000))
    return random.random()

# Anfällig: Kombination schwacher Quellen
def anfällig_kombinierter_seed():
    # Anfällig: XOR/Kombination schwacher Quellen ist immer noch schwach
    # Gesamtentropie ist durch schwächstes Glied begrenzt
    seed = int(time.time()) ^ os.getpid() ^ os.getppid()
    random.seed(seed)
    return random.randint(0, 2**64)

# Anfällig: Speicheradress-Seed
def anfällig_adress_seed():
    # Anfällig: Adressraum ist begrenzt, ASLR bietet ~28 Bits
    obj = object()
    random.seed(id(obj))
    return random.getrandbits(64)

# Anfällig: Benutzereingabe im Seed
def anfällig_benutzer_seed(user_id, timestamp):
    # Anfällig: Beide Werte sind bekannt/vorhersagbar
    random.seed(user_id * 1000000 + int(timestamp))
    return format(random.getrandbits(64), '016x')

# Anfällig: Zählerbasierter Seed
class AnfälligerZählerSeed:
    zähler = 0

    @classmethod
    def generieren(cls):
        cls.zähler += 1
        # Anfällig: Zähler ist vollständig vorhersagbar
        random.seed(cls.zähler)
        return random.getrandbits(64)
// Anfällig: Vorhersagbare PRNG-Seeds in Java
import java.util.Random;

public class AnfälligerVorhersagbarerSeed {

    // Anfällig: System.currentTimeMillis() Seed
    public long anfälligZeitSeed() {
        // Anfällig: ~1000 Werte pro Sekunde aufzuzählen
        Random rand = new Random(System.currentTimeMillis());
        return rand.nextLong();
    }

    // Anfällig: System.nanoTime() Seed
    public long anfälligNanoSeed() {
        // Anfällig: Immer noch vorhersagbar innerhalb Zeitfenster
        Random rand = new Random(System.nanoTime());
        return rand.nextLong();
    }

    // Anfällig: Thread-ID Seed
    public long anfälligThreadSeed() {
        // Anfällig: Thread-IDs sind vorhersagbar
        Random rand = new Random(Thread.currentThread().getId());
        return rand.nextLong();
    }

    // Anfällig: Hashcode-basierter Seed
    public long anfälligHashSeed(Object obj) {
        // Anfällig: Hashcode ist deterministisch
        Random rand = new Random(obj.hashCode());
        return rand.nextLong();
    }

    // Anfällig: Kombinierte vorhersagbare Quellen
    public long anfälligKombinierterSeed() {
        // Anfällig: Kombination schwacher Quellen
        long seed = System.currentTimeMillis() ^
                    Thread.currentThread().getId() ^
                    Runtime.getRuntime().freeMemory();
        return new Random(seed).nextLong();
    }

    // Anfällig: Bereitstellungszeit-Seed (echtes CVE-Muster)
    private static final long DEPLOY_TIME = System.currentTimeMillis();

    public String anfälligDeploySeed() {
        // Anfällig: Bereitstellungszeit ist ermittelbar
        // Alle zusammen bereitgestellten Instanzen verwenden gleichen Seed
        Random rand = new Random(DEPLOY_TIME);
        return Long.toHexString(rand.nextLong());
    }

    // Anfällig: Benutzer-Session-Seed
    public long anfälligSessionSeed(long userId, long loginTime) {
        // Anfällig: Beide Werte sind bekannt
        Random rand = new Random(userId ^ loginTime);
        return rand.nextLong();
    }
}
// Anfällig: Vorhersagbare PRNG-Seeds in C
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>

// Anfällig: time(NULL) Seed
void anfällig_zeit_seed() {
    // Anfällig: Ändert sich nur einmal pro Sekunde
    srand(time(NULL));
}

// Anfällig: PID Seed
void anfällig_pid_seed() {
    // Anfällig: PID ist 15-16 Bits
    srand(getpid());
}

// Anfällig: Thread-ID Seed
void anfällig_thread_seed() {
    // Anfällig: Thread-ID ist vorhersagbar
    srand((unsigned int)pthread_self());
}

// Anfällig: Zeit + PID (immer noch schwach)
void anfällig_zeit_pid_seed() {
    // Anfällig: Kombiniert, aber immer noch begrenzte Entropie
    // Bestenfalls ~32 Bits insgesamt
    srand(time(NULL) ^ getpid());
}

// Anfällig: Adressbasierter Seed
void anfällig_stack_seed() {
    // Anfällig: ASLR bietet begrenzte Entropie
    int stack_var;
    srand((unsigned int)&stack_var);
}

// Anfällig: Clock-Ticks Seed
void anfällig_clock_seed() {
    // Anfällig: Vorhersagbar basierend auf Prozesslebensdauer
    srand(clock());
}

// Anfällig: gettimeofday Seed
void anfällig_gettimeofday_seed() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    // Anfällig: Mikrosekunden sind im Zeitfenster immer noch vorhersagbar
    srand(tv.tv_sec * 1000000 + tv.tv_usec);
}

// Anfällig: Kombinierte schwache Quellen (Router-CVE-Muster)
void anfällig_router_seed() {
    // Anfällig: Muster aus CVE-2016-10180
    srand(time(0));  // Router-PIN-Generierung
}

Korrigierter Code (Python/Java)

# Korrigiert: Unvorhersagbare PRNG-Seeds
import secrets
import os

# Korrigiert: secrets-Modul verwenden - kein manuelles Seeding nötig
def sicheres_token():
    return secrets.token_hex(32)

# Korrigiert: Sicherer Seed aus System-Entropie
def sicher_geseedeter_random():
    import random
    gen = random.Random()
    # Korrigiert: 256 Bits aus System-Entropie
    gen.seed(os.urandom(32))
    return gen

# Korrigiert: Für Request-Behandlung - CSPRNG verwenden
def sicheres_request_token():
    # Korrigiert: Unabhängig von Request-Timing
    return secrets.token_urlsafe(32)

# Korrigiert: Hochpräzises Timing nicht benötigt
def sicherer_zufallswert():
    # Korrigiert: Ordnungsgemäße Entropiequelle verwenden
    return secrets.randbits(128)

# Korrigiert: Ordnungsgemäße Kombination von Entropie
def sichere_kombinierte_entropie():
    # Korrigiert: Mit stärker Entropie beginnen, kann mehr hinzufügen
    import hashlib
    basis_entropie = os.urandom(32)

    # Zusätzliche Entropiequellen (optional, nicht erforderlich)
    import time
    zusätzlich = str(time.time_ns()).encode()

    # Hash-Kombination gewährleistet mindestens Basis-Entropie
    kombiniert = hashlib.sha256(basis_entropie + zusätzlich).digest()
    return kombiniert

# Korrigiert: Speichersicherer Zufall
def sicher_speicher_sicher():
    # Korrigiert: Keine Speicheradressen verwenden
    return os.urandom(32)

# Korrigiert: Benutzerunabhängiger Zufall
def sicheres_benutzer_token(user_id):
    # Korrigiert: Benutzer-ID beeinflusst Entropie nicht
    del user_id  # Nicht für Seeding verwendet
    return secrets.token_hex(32)

# Korrigiert: Zähler beeinflusst Entropie nicht
class SichererZähler:
    def __init__(self):
        self.zähler = 0

    def generieren(self):
        self.zähler += 1
        # Korrigiert: Zähler ist nur eine ID, kein Seed
        token = secrets.token_hex(16)
        return f"{self.zähler}:{token}"
// Korrigiert: Unvorhersagbare PRNG-Seeds in Java
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

public class SichererVorhersagbarerSeed {

    // Korrigiert: SecureRandom verwenden, das aus System-Entropie seeded
    public long sichererZufallLong() throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstanceStrong();
        return sr.nextLong();
    }

    // Korrigiert: Kein zeitbasiertes Seeding
    public byte[] sicheresToken(int lange) throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[lange];
        sr.nextBytes(token);
        return token;
    }

    // Korrigiert: Thread-sicherer sicherer Zufall
    private static final ThreadLocal<SecureRandom> threadLocalRandom =
        ThreadLocal.withInitial(() -> {
            try {
                return SecureRandom.getInstanceStrong();
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
        });

    public long sichererThreadRandom() {
        return threadLocalRandom.get().nextLong();
    }

    // Korrigiert: Kein objektbasiertes Seeding
    public byte[] sicheresObjektToken(int lange) throws NoSuchAlgorithmException {
        // Korrigiert: Objekt beeinflusst Seeding nicht
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[lange];
        sr.nextBytes(token);
        return token;
    }

    // Korrigiert: Instanz-Erstellungszeit spielt keine Rolle
    public class SichererGenerator {
        private final SecureRandom secureRandom;

        public SichererGenerator() throws NoSuchAlgorithmException {
            // Korrigiert: SecureRandom behandelt Seeding ordnungsgemäß
            this.secureRandom = SecureRandom.getInstanceStrong();
        }

        public String generateToken() {
            byte[] bytes = new byte[32];
            secureRandom.nextBytes(bytes);
            return bytesToHex(bytes);
        }

        private String bytesToHex(byte[] bytes) {
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

    // Korrigiert: Session unabhängig von Benutzer/Zeit
    public byte[] sicheresSessionToken() throws NoSuchAlgorithmException {
        // Korrigiert: Benutzer/Login-Zeit nicht für Seeding verwendet
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[32];
        sr.nextBytes(token);
        return token;
    }
}
// Korrigiert: Unvorhersagbare PRNG-Seeds in C
#include <openssl/rand.h>
#include <fcntl.h>
#include <unistd.h>

// Korrigiert: Aus /dev/urandom seeden
int sicher_init() {
    unsigned char seed[32];

    int fd = open("/dev/urandom", O_RDONLY);
    if (fd < 0) return -1;

    if (read(fd, seed, sizeof(seed)) != sizeof(seed)) {
        close(fd);
        return -1;
    }
    close(fd);

    RAND_seed(seed, sizeof(seed));
    OPENSSL_cleanse(seed, sizeof(seed));

    return 0;
}

// Korrigiert: RAND_bytes verwenden
int sichere_zufalls_bytes(unsigned char *buffer, size_t lange) {
    return RAND_bytes(buffer, lange) == 1 ? 0 : -1;
}

// Korrigiert: Kein zeitbasiertes Seeding
int sicheres_token(unsigned char *token, size_t lange) {
    // Korrigiert: Zeit nicht verwendet
    return sichere_zufalls_bytes(token, lange);
}

// Korrigiert: Kein PID-Seeding
int sichere_session_id(unsigned char *session_id, size_t lange) {
    // Korrigiert: PID nicht verwendet
    return sichere_zufalls_bytes(session_id, lange);
}

// Korrigiert: getrandom unter Linux verwenden
#ifdef __linux__
#include <sys/random.h>

int sicher_getrandom(unsigned char *buffer, size_t lange) {
    ssize_t result = getrandom(buffer, lange, 0);
    return result == (ssize_t)lange ? 0 : -1;
}
#endif

// Korrigiert: Ordnungsgemäßer Router-Zufall (CVE-Muster behoben)
int sicherer_router_pin(char *pin, size_t max_len) {
    unsigned char random_bytes[4];

    if (sichere_zufalls_bytes(random_bytes, sizeof(random_bytes)) != 0) {
        return -1;
    }

    // In PIN konvertieren mit zufälligen Bytes, nicht Zeit
    unsigned int pin_wert =
        (random_bytes[0] << 24) |
        (random_bytes[1] << 16) |
        (random_bytes[2] << 8) |
        random_bytes[3];

    snprintf(pin, max_len, "%08u", pin_wert % 100000000);
    return 0;
}

Die Korrektur verwendet System-Entropiequellen anstelle vorhersagbarer Werte für das Seeding.


Ausgenutzt in der Praxis

Debian OpenSSL (CVE-2008-0166)

Das Debian-OpenSSL-Paket verwendete nur die Prozess-ID für PRNG-Seeding und generierte so nur 65.536 eindeutige Schlüssel auf allen betroffenen Systemen.

Kubernetes Cloud-App (CVE-2020-7010)

Eine Kubernetes-Cloud-Anwendung generierte Passwörter mit einem RNG, der mit der Bereitstellungszeit geseeded wurde, was die Vorhersage von Anmeldedaten ermöglichte.


Tools zum Testen/Ausnutzen


CVE-Beispiele


Referenzen

  1. MITRE Corporation. "CWE-337: Predictable Seed in PRNG." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/337.html

  2. Goldberg, I., Wagner, D. "Randomness and the Netscape Browser." Dr. Dobb's Journal, 1996.

  3. NIST. "Recommendation for Random Number Generation." SP 800-90A. https://csrc.nist.gov/publications/detail/sp/800-90a/rev-1/final