Predictable Seed in Pseudo-Random Number Generator (PRNG)

Description

Predictable Seed in Pseudo-Random Number Generator (PRNG) is a vulnerability that occurs when a PRNG is initialized with a seed value that an attacker can predict or determine. Common predictable seed sources include system time (often with second precision), process IDs, thread IDs, memory addresses, or combinations of these values. Since PRNGs produce deterministic output based on their seed, a predictable seed dramatically reduces the search space an attacker must explore to predict generated values. Instead of searching an astronomical number of possible random sequences, attackers need only consider the relatively small set of possible seed values.

Risk

Predictable seeds transform theoretical security into practical vulnerability. System time seeds typically reduce the seed space to thousands or millions of values (seconds or milliseconds in a time window), compared to the theoretical 2^128 or more possible values. Process IDs are typically 15-16 bits (32,768 to 65,536 values). When attackers can estimate when a seed was generated (application startup, session creation, etc.), they can enumerate all possible seeds in seconds or minutes. Real-world exploits have used predictable seeds to compromise cryptocurrency wallets (CVE-2020-7010), predict session tokens, forge authentication codes, and break encryption. The Debian OpenSSL bug (CVE-2008-0166) where only process ID was used for seeding affected thousands of systems and remains one of the most impactful cryptographic vulnerabilities in history.

Solution

Seed PRNGs exclusively from cryptographically secure, unpredictable entropy sources. Use operating system-provided CSPRNGs (/dev/urandom, CryptGenRandom, SecureRandom.getInstanceStrong()) that gather entropy from hardware sources and system events. Never derive seeds from system time, process IDs, memory addresses, or other predictable system state. If you must use a statistical PRNG for performance reasons, seed it from a CSPRNG with at least 256 bits of entropy. Implement defense in depth by combining multiple independent entropy sources when available. Consider hardware random number generators for high-security applications. Test for predictable seeding by auditing initialization code and using static analysis tools.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Attackers can predict authentication tokens, session IDs, and passwords generated with predictably-seeded PRNGs, bypassing authentication and gaining unauthorized access.
ConfidentialityScope: Confidentiality

Cryptographic keys generated from predictable seeds can be reconstructed through seed enumeration, compromising encrypted data.
IntegrityScope: Integrity

CSRF tokens, nonces, and other integrity mechanisms become predictable and forgeable when based on predictably-seeded PRNGs.

Example Code

Vulnerable Code (Python/Java)

The following examples demonstrate predictable seed vulnerabilities:

# Vulnerable: Predictable PRNG seeds
import random
import time
import os

# Vulnerable: Current time as seed
def vulnerable_time_seed():
    # Vulnerable: time.time() is predictable
    # Attacker knowing approximate time narrows to thousands of values
    random.seed(time.time())
    return random.getrandbits(128)

# Vulnerable: Process ID as seed
def vulnerable_pid_seed():
    # Vulnerable: PID is only 15-16 bits
    # Enumerable in milliseconds
    random.seed(os.getpid())
    return random.getrandbits(64)

# Vulnerable: Timestamp from request
def vulnerable_request_time(request_timestamp):
    # Vulnerable: Attacker controls or knows timestamp
    random.seed(int(request_timestamp))
    return format(random.getrandbits(64), '016x')

# Vulnerable: Microsecond time (slightly better but still weak)
def vulnerable_microsecond_seed():
    # Vulnerable: Still only ~20 bits of entropy per second
    random.seed(int(time.time() * 1000000))
    return random.random()

# Vulnerable: Combining weak sources
def vulnerable_combined_seed():
    # Vulnerable: XOR/combination of weak sources is still weak
    # Total entropy is bounded by weakest link
    seed = int(time.time()) ^ os.getpid() ^ os.getppid()
    random.seed(seed)
    return random.randint(0, 2**64)

# Vulnerable: Memory address seed
def vulnerable_address_seed():
    # Vulnerable: Address space is limited, ASLR provides ~28 bits
    obj = object()
    random.seed(id(obj))
    return random.getrandbits(64)

# Vulnerable: User input in seed
def vulnerable_user_seed(user_id, timestamp):
    # Vulnerable: Both values are known/predictable
    random.seed(user_id * 1000000 + int(timestamp))
    return format(random.getrandbits(64), '016x')

# Vulnerable: Counter-based seed
class VulnerableCounterSeed:
    counter = 0

    @classmethod
    def generate(cls):
        cls.counter += 1
        # Vulnerable: Counter is entirely predictable
        random.seed(cls.counter)
        return random.getrandbits(64)
// Vulnerable: Predictable PRNG seeds in Java
import java.util.Random;

public class VulnerablePredictableSeed {

    // Vulnerable: System.currentTimeMillis() seed
    public long vulnerableTimeSeed() {
        // Vulnerable: ~1000 values per second to enumerate
        Random rand = new Random(System.currentTimeMillis());
        return rand.nextLong();
    }

    // Vulnerable: System.nanoTime() seed
    public long vulnerableNanoSeed() {
        // Vulnerable: Still predictable within time window
        Random rand = new Random(System.nanoTime());
        return rand.nextLong();
    }

    // Vulnerable: Thread ID seed
    public long vulnerableThreadSeed() {
        // Vulnerable: Thread IDs are predictable
        Random rand = new Random(Thread.currentThread().getId());
        return rand.nextLong();
    }

    // Vulnerable: Hashcode-based seed
    public long vulnerableHashSeed(Object obj) {
        // Vulnerable: Hashcode is deterministic
        Random rand = new Random(obj.hashCode());
        return rand.nextLong();
    }

    // Vulnerable: Combined predictable sources
    public long vulnerableCombinedSeed() {
        // Vulnerable: Combination of weak sources
        long seed = System.currentTimeMillis() ^
                    Thread.currentThread().getId() ^
                    Runtime.getRuntime().freeMemory();
        return new Random(seed).nextLong();
    }

    // Vulnerable: Deployment time seed (real CVE pattern)
    private static final long DEPLOY_TIME = System.currentTimeMillis();

    public String vulnerableDeploySeed() {
        // Vulnerable: Deployment time is discoverable
        // All instances deployed together use same seed
        Random rand = new Random(DEPLOY_TIME);
        return Long.toHexString(rand.nextLong());
    }

    // Vulnerable: User session seed
    public long vulnerableSessionSeed(long userId, long loginTime) {
        // Vulnerable: Both values are known
        Random rand = new Random(userId ^ loginTime);
        return rand.nextLong();
    }
}
// Vulnerable: Predictable PRNG seeds in C
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>

// Vulnerable: time(NULL) seed
void vulnerable_time_seed() {
    // Vulnerable: Only changes once per second
    srand(time(NULL));
}

// Vulnerable: PID seed
void vulnerable_pid_seed() {
    // Vulnerable: PID is 15-16 bits
    srand(getpid());
}

// Vulnerable: Thread ID seed
void vulnerable_thread_seed() {
    // Vulnerable: Thread ID is predictable
    srand((unsigned int)pthread_self());
}

// Vulnerable: Time + PID (still weak)
void vulnerable_time_pid_seed() {
    // Vulnerable: Combined but still limited entropy
    // ~32 bits total at best
    srand(time(NULL) ^ getpid());
}

// Vulnerable: Address-based seed
void vulnerable_stack_seed() {
    // Vulnerable: ASLR provides limited entropy
    int stack_var;
    srand((unsigned int)&stack_var);
}

// Vulnerable: Clock ticks seed
void vulnerable_clock_seed() {
    // Vulnerable: Predictable based on process lifetime
    srand(clock());
}

// Vulnerable: gettimeofday seed
void vulnerable_gettimeofday_seed() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    // Vulnerable: Microseconds still predictable in time window
    srand(tv.tv_sec * 1000000 + tv.tv_usec);
}

// Vulnerable: Combined weak sources (router CVE pattern)
void vulnerable_router_seed() {
    // Vulnerable: Pattern from CVE-2016-10180
    srand(time(0));  // Router PIN generation
}

Fixed Code (Python/Java)

# Fixed: Unpredictable PRNG seeds
import secrets
import os

# Fixed: Use secrets module - no manual seeding needed
def secure_token():
    return secrets.token_hex(32)

# Fixed: Secure seed from system entropy
def secure_seeded_random():
    import random
    gen = random.Random()
    # Fixed: 256 bits from system entropy
    gen.seed(os.urandom(32))
    return gen

# Fixed: For request handling - use CSPRNG
def secure_request_token():
    # Fixed: Independent of request timing
    return secrets.token_urlsafe(32)

# Fixed: High-precision timing not needed
def secure_random_value():
    # Fixed: Use proper entropy source
    return secrets.randbits(128)

# Fixed: Proper combination of entropy
def secure_combined_entropy():
    # Fixed: Start with strong entropy, can add more
    import hashlib
    base_entropy = os.urandom(32)

    # Additional entropy sources (optional, not required)
    import time
    additional = str(time.time_ns()).encode()

    # Hash combination ensures at least base entropy
    combined = hashlib.sha256(base_entropy + additional).digest()
    return combined

# Fixed: Memory-safe random
def secure_memory_safe():
    # Fixed: Don't use memory addresses
    return os.urandom(32)

# Fixed: User-independent random
def secure_user_token(user_id):
    # Fixed: User ID doesn't affect entropy
    del user_id  # Not used for seeding
    return secrets.token_hex(32)

# Fixed: Counter doesn't affect entropy
class SecureCounter:
    def __init__(self):
        self.counter = 0

    def generate(self):
        self.counter += 1
        # Fixed: Counter is just an ID, not a seed
        token = secrets.token_hex(16)
        return f"{self.counter}:{token}"
// Fixed: Unpredictable PRNG seeds in Java
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;

public class SecurePredictableSeed {

    // Fixed: Use SecureRandom which seeds from system entropy
    public long secureRandomLong() throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstanceStrong();
        return sr.nextLong();
    }

    // Fixed: No time-based seeding
    public byte[] secureToken(int length) throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[length];
        sr.nextBytes(token);
        return token;
    }

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

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

    // Fixed: No object-based seeding
    public byte[] secureObjectToken(int length) throws NoSuchAlgorithmException {
        // Fixed: Object doesn't affect seeding
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[length];
        sr.nextBytes(token);
        return token;
    }

    // Fixed: Instance creation time doesn't matter
    public class SecureGenerator {
        private final SecureRandom secureRandom;

        public SecureGenerator() throws NoSuchAlgorithmException {
            // Fixed: SecureRandom handles seeding properly
            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();
        }
    }

    // Fixed: Session independent of user/time
    public byte[] secureSessionToken() throws NoSuchAlgorithmException {
        // Fixed: User/login time not used for seeding
        SecureRandom sr = SecureRandom.getInstanceStrong();
        byte[] token = new byte[32];
        sr.nextBytes(token);
        return token;
    }
}
// Fixed: Unpredictable PRNG seeds in C
#include <openssl/rand.h>
#include <fcntl.h>
#include <unistd.h>

// Fixed: Seed from /dev/urandom
int secure_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;
}

// Fixed: Use RAND_bytes
int secure_random_bytes(unsigned char *buffer, size_t length) {
    return RAND_bytes(buffer, length) == 1 ? 0 : -1;
}

// Fixed: No time-based seeding
int secure_token(unsigned char *token, size_t length) {
    // Fixed: Time not used
    return secure_random_bytes(token, length);
}

// Fixed: No PID seeding
int secure_session_id(unsigned char *session_id, size_t length) {
    // Fixed: PID not used
    return secure_random_bytes(session_id, length);
}

// Fixed: Using getrandom on Linux
#ifdef __linux__
#include <sys/random.h>

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

// Fixed: Proper router random (fixing CVE pattern)
int secure_router_pin(char *pin, size_t max_len) {
    unsigned char random_bytes[4];

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

    // Convert to PIN using random bytes, not time
    unsigned int pin_value =
        (random_bytes[0] << 24) |
        (random_bytes[1] << 16) |
        (random_bytes[2] << 8) |
        random_bytes[3];

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

The fix uses system entropy sources rather than predictable values for seeding.


Exploited in the Wild

Debian OpenSSL (CVE-2008-0166)

The Debian OpenSSL package used only process ID for PRNG seeding, generating only 65,536 unique keys across all affected systems.

Kubernetes Cloud App (CVE-2020-7010)

A Kubernetes cloud application generated passwords using an RNG seeded with deployment time, allowing prediction of credentials.


Tools to Test/Exploit


CVE Examples


References

  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