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
| Auswirkung | Details |
|---|---|
| Zugriffskontrolle | Umfang: 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. |
| Vertraulichkeit | Umfang: 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ät | Umfang: 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
-
untwister — Stellt PRNG-Seeds aus beobachteten Ausgaben wieder her.
-
Statische Analyse-Tools — Erkennen zeit/PID-basierte Seeding-Muster.
-
Seed-Aufzählungsskripte — Zählen mögliche Seeds in Zeitfenstern auf.
CVE-Beispiele
-
CVE-2008-0166 — Debian OpenSSL nur PID-Seeding.
-
CVE-2016-10180 — Router-PIN mit rand(time(0)).
-
CVE-2018-9057 — Cloud-Produkt mit zeitgeseedetem nicht-krypto PRNG.
Referenzen
-
MITRE Corporation. "CWE-337: Predictable Seed in PRNG." Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/337.html
-
Goldberg, I., Wagner, D. "Randomness and the Netscape Browser." Dr. Dobb's Journal, 1996.
-
NIST. "Recommendation for Random Number Generation." SP 800-90A. https://csrc.nist.gov/publications/detail/sp/800-90a/rev-1/final