A pseudorandom number generator (PRNG) is a deterministic algorithm capable of generating sequences of numbers that approximate the properties of random numbers. Each sequence is completely determined by the initial state of the PRNG and the algorithm for changing the state. Most PRNGs make it possible to set the initial state, also called the seed state. Setting the initial state is called seeding the PRNG.

Calling a PRNG in the same initial state, either without seeding it explicitly or by seeding it with the same value, results in generating the same sequence of random numbers in different runs of the program. Consider a PRNG function that is seeded with some initial seed value and is consecutively called to produce a sequence of random numbers, S. If the PRNG is subsequently seeded with the same initial seed value, then it will generate the same sequence S.

As a result, after the first run of an improperly seeded PRNG, an attacker can predict the sequence of random numbers that will be generated in the future runs. Improperly seeding or failing to seed the PRNG can lead to vulnerabilities, especially in security protocols.

The solution is to ensure that the PRNG is always properly seeded. A properly seeded PRNG will generate a different sequence of random numbers each time it is run.

Not all random number generators can be seeded. True random number generators that rely on hardware to produce completely unpredictable results do not need to be and cannot be seeded. Some high-quality PRNGs, such as the /dev/random device on some UNIX systems, also cannot be seeded. This rule applies only to algorithmic pseudorandom number generators that can be seeded.

Noncompliant Code Example (POSIX)

This noncompliant code example generates a sequence of 10 pseudorandom numbers using the random() function. When random() is not seeded, it behaves like rand(), producing the same sequence of random numbers each time any program that uses it is run.

#include <stdio.h>
#include <stdlib.h>
 
void func(void) {
  for (unsigned int i = 0; i < 10; ++i) {
    /* Always generates the same sequence */
    printf("%ld, ", random());
  }
}

The output is as follows:

1st run: 1804289383, 846930886, 1681692777, 1714636915, 1957747793, 424238335, 719885386, 1649760492, 596516649,
         1189641421,
2nd run: 1804289383, 846930886, 1681692777, 1714636915, 1957747793, 424238335, 719885386, 1649760492, 596516649,
         1189641421,
...
nth run: 1804289383, 846930886, 1681692777, 1714636915, 1957747793, 424238335, 719885386, 1649760492, 596516649,
         1189641421,

Compliant Solution (POSIX)

Call srandom() before invoking random() to seed the random sequence generated by random(). This compliant solution produces different random number sequences each time the function is called, depending on the resolution of the system clock:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
void func(void) {
  struct timespec ts;
  if (timespec_get(&ts, TIME_UTC) == 0) {
    /* Handle error */
  } else {
    srandom(ts.tv_nsec ^ ts.tv_sec);
    for (unsigned int i = 0; i < 10; ++i) {
      /* Generates different sequences at different runs */
      printf("%ld, ", random());
    }
  }
}

The output is as follows:

1st run: 198682410, 2076262355, 910374899, 428635843, 2084827500, 1558698420, 4459146, 733695321, 2044378618, 1649046624,
2nd run: 1127071427, 252907983, 1358798372, 2101446505, 1514711759, 229790273, 954268511, 1116446419, 368192457,
         1297948050,
3rd run: 2052868434, 1645663878, 731874735, 1624006793, 938447420, 1046134947, 1901136083, 418123888, 836428296,
         2017467418,

This may not be sufficiently random for concurrent execution, which may lead to correlated generated series in different threads. Depending on the application and the desired level of security, a programmer may choose alternative ways to seed PRNGs. In general, hardware is more capable than software of generating real random numbers (for example, by sampling the thermal noise of a diode).

Compliant Solution (Windows)

The BCryptGenRandom() function does not run the risk of not being properly seeded because its arguments serve as seeders:

#include <stdio.h>
#include <Windows.h>
#include <Bcrypt.h>
#include <Ntstatus.h>
#include <Wincrypt.h>

void func(void) {
  BCRYPT_ALG_HANDLE hAlgorithm = NULL;
  long rand_buf;
  PUCHAR pbBuffer = (PUCHAR) &rand_buf;
  ULONG cbBuffer = sizeof(rand_buf);
  ULONG dwFlags = BCRYPT_USE_SYSTEM_PREFERRED_RNG;
  NTSTATUS status;
  for (unsigned int i = 0; i < 10; ++i) {
    status = BCryptGenRandom(hAlgorithm, pbBuffer, cbBuffer, dwFlags);
    if (status == STATUS_SUCCESS) {
      printf("%ld, ", rand_buf);
    } else {
      /* Handle Error */
    }
  }
}

The output is as follows:

1st run: -683378946, 1957231690, 1933176011, -1745403355, -883473417, 882992405, 169629816, 1824800038, 899851668, 1702784647, 
2nd run: -58750553, -1921870721, -1973269161, 1512649964, -673518452, 234003619, -1622633366, 1312389688, -2125631172, 2067680022, 
3rd run: -189899579, 1220698973, 752205360, -1826365616, 79310867, 1430950090, -283206168, -941773185, 129633665, 543448789, 

Risk Assessment

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

MSC32-C

Medium

Likely

Low

P18

L1

Automated Detection

Tool

Version

Checker

Description

Astrée
24.04

Supported, but no explicit checker
Axivion Bauhaus Suite

7.2.0

CertC-MSC32
CodeSonar
8.1p0

HARDCODED.SEED
MISC.CRYPTO.TIMESEED

Hardcoded Seed in PRNG
Predictable Seed in PRNG

Cppcheck Premium

24.11.0

premium-cert-msc32-c
Helix QAC

2024.4

C5031

C++5036


Klocwork
2024.4

CERT.MSC.SEED_RANDOM


PC-lint Plus

1.4

2460, 2461, 2760

Fully supported

Polyspace Bug Finder

R2024a

CERT C: Rule MSC32-C


Checks for:

  • Deterministic random output from constant seed
  • Predictable random output from predictable seed

Rule fully covered.

Parasoft C/C++test

2023.1

CERT_C-MSC32-d

Properly seed pseudorandom number generators

Related Vulnerabilities

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

Related Guidelines

Key here (explains table format and definitions)

Taxonomy

Taxonomy item

Relationship

CERT C Secure Coding StandardMSC30-C. Do not use the rand() function for generating pseudorandom numbersPrior to 2018-01-12: CERT: Unspecified Relationship
CERT CMSC51-CPP. Ensure your random number generator is properly seededPrior to 2018-01-12: CERT: Unspecified Relationship
CWE 2.11CWE-327, Use of a Broken or Risky Cryptographic Algorithm2017-05-16: CERT: Rule subset of CWE
CWE 2.11CWE-330, Use of Insufficiently Random Values2017-06-28: CERT: Rule subset of CWE
CWE 2.11CWE-331, Insufficient Entropy2017-06-28: CERT: Exact

CERT-CWE Mapping Notes

Key here for mapping notes

CWE-327 and MSC32-C


  • Intersection( MSC30-C, MSC32-C) = Ø



  • MSC32-C says to properly seed pseudorandom number generators. For example, if you call rand(), make sure to seed it properly by calling srand() first. So far, we haven’t found any calls to rand().



  • Failure to seed a PRNG causes it to produce reproducible (hence insecure) series of random numbers.



  • CWE-327 = Union( MSC32-C, list) where list =



  • Invocation of broken/risky crypto algorithms that are not properly seeded




CWE-330 and MSC32-C

Independent( MSC30-C, MSC32-C, CON33-C)

CWE-330 = Union( MSC30-C, MSC32-C, CON33-C, list) where list = other improper use or creation of random values. (EG the would qualify)

MSC30-C, MSC32-C and CON33-C are independent, they have no intersections. They each specify distinct errors regarding PRNGs.

Bibliography




27 Comments

    1. MSC30-C addresses the problem that numbers generated by rand() have a comparatively short cycle, meaning that the numbers may be predictable. Suppose we have the following code:

      for(int i=0; i<RAND_MAX; i++){
         printf("%d, ", rand());
      }
      

      After some iterations (I believe at about RAND_MAX/2 - according to birthday paradox), we will start having collisions. Right?
       
      MSC18-C is quite different from MSC30-C in the following sense:
      MSC30-C: Calling rand() in a large for loop will sooner or later result in generating the same random numbers.

      MSC18-C: Calling rand() to create a sequence of random numbers will always result in the same sequence generated at different runs of the program. For example, suppose we want to create a sequence of 10 random numbers and thus we write the following code:

      for(int i=0; i<10; i++){
         printf("%d, ", rand());
      }
      


      Running our program will produce let's say: 41, 18467, 6334, 265000, 19169, 15724, 11478, 29358, 26962, 24464 (actually this is a real sequence generated when I run the program)

      Running the same program a second time will produce the exact same sequence. More generally, any subsequent runs of the program will generate the same sequence.

      If program changes to:

      srand(time(NULL));
      
      for(int i=0; i<10; i++){
      
         printf("%d, ", rand());
      
      }
      

      then different runs of the program will produce different sequences of random numbers.

      The same thing holds for random() for POSIX, too! And the solution is also the same. Use of srandom() seeds random() to produce different sequences at different runs of the program. I have not written this yet, but I will do so soon. Again:

      MSC30-C: Mentions that random() generates random numbers generated with a bigger cycle.

      MSC18-C: Mentions that when come to sequences of random numbers generated in different runs of the program, then random() has the same behavior with rand() and should be properly seeded.

      I will also check CryptGenRandom() for Windows.

      From a security perspective,  MSC18-C is different in the following sense:

      MSC30-C: A malicious user should have to wait a quite reasonably amount of time before starting predicting (with some probability) patterns of generated random numbers.

      MSC18-C: After the first run of the program, a malicious user will know the sequence of random numbers to be generated in any subsequent runs.

      If our program is a game where the player has to find hidden treasures, then the next time a  user plays, (s)he will know where the treasures are and (s)he can cheat!

      In a worse case, suppose that Alice and Bob want to communicate 10 times per day. Suppose also, that they use asymmetric cryptography (Diffie-Hellman, or El-Gamal, or any other protocol where parties pick some random numbers). Suppose Bob and Alice run a program to generate a sequence of 10 random numbers. Each number produced will be used in the corresponding communication in between them, i.e. 1st number will be used for their first communication and then dropped, 2nd number for their second communication and then dropped, etc. The problem is that if Bob and Alice run the same program the next day to produce another sequence of random numbers, then the sequence generated will be the same! On the other hand, if Alice and Bob use srand() to seed rand(), then different sequences will be produced.

      Both MSC30-C and MSC18-C refere to the problem of randomness, but from a different point of view and by transferring the time window until collision takes place. Vulnerable time window for MSC18-C will be just the next run!!!

      Taking all the above into account, I am starting thinking if this should be a rule instead of just a recommendation. What do you think about it?

      1. To be honest, this might make a good rule, rather than a recommendation. It is universally applicable (srand() can seed either randomely or deterministically), forgetting to seed will yield a vul, and it is not that difficult to enforce automatically (and it is a snap to check with dynamic analysis).

        There may still be a good reason this should be a recommendation, not a rule, but I can't think of it, and IMHO the best way to find it is to make this a rule and find someone who knows more about random numbers than us to criticize it (smile)

        So go ahead & make it a rule, not a rec.

  1. This rule is not quite valid, because MSC30-C depreates the use of rand(), recommending random() for POSIX and CryptGenRandom() for Windows. So you shouldn't be advocating proper usage of rand().

    However, you can make valid recommendation out of this by generalizing it to something like "ensure your random number generator is properly seeded". MSC30-C doesn't address seeding (though the code examples do seed properly).

    That done, there's just a few other things you'll need:

    • Fill out the Risk Assessment
    • A link to MSC30-C, since it is relevant.
    1. It was not my intention to advocate proper usage for deprecated rand(). My objective was to address the problem of generating different sequences of random numbers. The same problem holds for rand(), random() and I believe this will also be the case for CryptGenRandom(). I will check and update MSC18-C soon.

      I also think that MSC18-C should be changed in something like "ensure your random number generator is properly seeded". Shall I do this?

      I would also like to ask how the  Risk Assessment table is going to be filled. In adition, what about Automated Detection and Related Vulnerabilities? Any hints?

      1. wrt "ensure your RNS is properly seeded", yes do that. Make it a rule, not a rec, as outlined in my earlier comment.

        As to your other questions, consult the wiki pages in the Introduction section; they specifically answer your question (as well as explain the differences between rules and recommendations.)

  2. Some caveats that you can try to address -

    • It is likely that if the seed repeats, so will the sequence. Since an attacker may be able to change the system time maliciously and set it back to a previous value, it might break one's crypto implementations (or whatever the RNG is being used for). It is arguable whether the attacker can change system time without superuser privileges, however, malicious broadcasts with NTP and such are not unheard of.
    • The programmer must never hardcode the seed or accept user controllable data
    • Since this solution is based on time as a seed, and time changes every second, the seed remains the same within the second. So the following code will again repeat the sequence. (So using srand() for every run, especially if the implementation requires the program to run several times every second, may not be a good idea)
    int main(void)
    {
      int i, j,stime;
      long ltime;
    
      /* get the current calendar time */
     for(i=0;i<10;i++) {
       ltime = time(NULL);
       stime = (unsigned) ltime/2;
       printf("t=%ld",ltime);
    
       srand(stime);
    
       for(j=0; j<10; j++) printf("%d ", rand());
       
       printf("\n");
     }
      return 0;
    }
    
    
    1. I'm not sure if seeding a RNG with the current time is theoretically vulnerable to an attacker modifying the time, since, due to the interval between time modification and RNG seeding, an attacker can't use time modification to pass a known value to a RNG seed.

      Still, if one is that concerned about randomization, one would not be using rand()/srand().

      Other points:

      • The rule and code examples still use rand()...use something else like random() in the code examples.
      • Mention MSC30-C, which explains why we can't use rand() securely.
      1. Since 'time' is predictable, and changes the sequence only after every second, it might not be hard to supply a stale time so that the same sequence is regenerated. (srand generates the same sequence when the seed is the same. An attacker can account for the expected network delay and set the time so that it matches a previous run of the protocol). But yes, the attacker would need to know the exact time when the protocol will be rerun. (which is possibly not hard, consider smartcards where the attacker can see the person swiping). Also consider, srand(time) will generate the same sequence everyday at that 'time'. I suspect it is fine to use rand() for casual use as you pointed out.

        The other issue was that suppose several processes are initiated at the same time. They will all end up using the same 'random' sequence due to the dependence on system time. Perhaps for *nix systems, one should have srand with (current time concatenated with process id) as a parameter to distinguish these?

        1. time() returns the current time counted as seconds since 01/01/1970. So, the srand(time(NULL)) will not generate the same sequence everyday at that 'time'.

          Moreover, I have changed the rule so that it mentions that time() is only used as an example. Seed to be used depends on the security level each application requires. Also, for real random numbers, hardware is better.

  3. I'm not sure how the compliant solution can make use of the rand() function given: MSC30-C. Do not use the rand() function for generating pseudorandom numbers

    1. I have completed the rule. The rule does address the seeding for all three functions, i.e. rand(), random() and CryptGenRandom(), mentioned in MSC30-C. Do not use the rand() function for generating pseudorandom numbers for completeness. I have the feeling that as the rule is written now, it both examines the case for seeding rand() and does not contradict to MSC30-C.

      Please take a look and let me know if further modifications are needed.

  4. The C Standard doesn't specify the encoding of time_t, so the comment
    /* Create seed based on current time counted as seconds from 01/01/1970 */
    in the first compliant solution is wrong. (It is okay in the POSIX solution.)

    1. So, do you think it is better to change the comment in the first compliant solution to something like /*  Create seed based on current time */? I can do this but I don't get it. OK, ISO C defines time_t as an arithmetic type, but does not specify any particular type, range, resolution or encoding for it. But if you run time(NULL) and convert the result to years and days, then you will find 'today' counted from 'Unix Epoch'. Do you think that a different C compiler would give another result?

      1. Niki, I agree with Geoff's recommendation. His point is that C makes no guarantee that time_t represents number of seconds since the epoch. While POSIX may guarantee that, your first compliant example purports to be standard C; thus it should not make any assumptions about the internals of time_t. It is possible, and legal, for a standards-compliant compiler to return a time_t that does not represent seconds from the epoch, and your standard-compliant example must take that possiblity into account.

      2. Changing the comment as you suggest would be fine.

        ISO C just requires time() to return a value that represents the current time in some way. The value does not have to be a count of some time unit such as seconds, and subtracting two time_t values does not have to produce anything meaningful - that's why the difftime() function exists.

  5. I get funny results in Linux with next code 

    size_t MyRandom(size_t id) {
    
          size_t result = 0;
          size_t i = 0;
    
          for (i = 0; i < 20; i++) {
              srand48((long int)time(NULL)); /* init seed */
             (lrand48()/rand()+1) % id;    
          }
     return ((lrand48()/rand()+1)) % id;
    };
    

    id - is maximal number which me need;

    20 - is experemental number (smile)

    On little id this function get good results! Why? I do not know!  (smile)

    1. Your results are probably funny because in POSIX the value returned by time(2) represents the number of seconds since the epoch. So calling time(2) twice during the same second returns the same result...consequently your seeds are the same and you'll therefore get the same random sequence in each loop iteration. As noted in previous comments, C99 only requires that time(2) return an integral type representing time, and it is POSIX that requires time(2)'s return value to be expressed in seconds.

      BTW my srand48(3) manpage says:

      NOTES
      These functions are declared obsolete by SVID 3, which states that rand(3) should be used
      instead.

  6. This rule sort of violates our guideline of having at least one NCE based on the C Standard.  In this case, that example would be rand(), but because we have a rule against using rand(), I decided to omit that example.

  7. A statement has been added to the POSIX CS saying "This may not be sufficiently random for ... small embedded systems that have an unsigned int type with a width of 16 bits."  POSIX specifies a minimum width of 32 bits for unsigned int, so this statement should either be removed or be extended to point out that such systems do not conform to POSIX.

      1. I'm still puzzling a bit over why we have this statement.  Were their earlier versions of POSIX which allowed 16 bit unsigned int?  Certainly C does.

        1. Yes, prior to the 2001 edition, POSIX.1 allowed 16 bit unsigned int.

          1. Thanks.  I added this history to the description.  It now makes more sense (to me) why we are discussing 16 bit unsigned int in this context.

  8. The compliant solution for Windows maybe should be updated according to the changes made for MSC30-C (CryptGenRandom() replaced by BCryptGenRandom()).

    1. Ryan Steele has provided us with a new compliant example using BCryptGenRandom().