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 | Hardcoded Seed in PRNG |
Helix QAC | 2024.2 | C5031 C++5036 | |
Klocwork | 2024.2 | CERT.MSC.SEED_RANDOM | |
PC-lint Plus | 1.4 | 2460, 2461, 2760 | Fully supported |
Polyspace Bug Finder | R2024a | Checks for:
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 Standard | MSC30-C. Do not use the rand() function for generating pseudorandom numbers | Prior to 2018-01-12: CERT: Unspecified Relationship |
CERT C | MSC51-CPP. Ensure your random number generator is properly seeded | Prior to 2018-01-12: CERT: Unspecified Relationship |
CWE 2.11 | CWE-327, Use of a Broken or Risky Cryptographic Algorithm | 2017-05-16: CERT: Rule subset of CWE |
CWE 2.11 | CWE-330, Use of Insufficiently Random Values | 2017-06-28: CERT: Rule subset of CWE |
CWE 2.11 | CWE-331, Insufficient Entropy | 2017-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.
27 Comments
Robert Seacord
How is this different from MSC30-C. Do not use the rand() function for generating pseudorandom numbers
Elpiniki Tsakalaki
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: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:
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:
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?
David Svoboda
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
So go ahead & make it a rule, not a rec.
David Svoboda
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:
Elpiniki Tsakalaki
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?
David Svoboda
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.)
Dhruv Mohindra
Some caveats that you can try to address -
David Svoboda
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:
Dhruv Mohindra
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?
Elpiniki Tsakalaki
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.
Robert Seacord
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 numbersElpiniki Tsakalaki
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.
Geoff Clare
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.)
Elpiniki Tsakalaki
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?David Svoboda
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.
Geoff Clare
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.Elpiniki Tsakalaki
OK, done!
Pavel Vasilyev
I get funny results in Linux with next code
id - is maximal number which me need;
20 - is experemental number
On little id this function get good results! Why? I do not know!
David Svoboda
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:
Robert Seacord (Manager)
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.
Geoff Clare
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 forunsigned int
, so this statement should either be removed or be extended to point out that such systems do not conform to POSIX.David Svoboda
Fixed, thanks.
Robert Seacord
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.
Geoff Clare
Yes, prior to the 2001 edition, POSIX.1 allowed 16 bit unsigned int.
Robert Seacord
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.
versat
The compliant solution for Windows maybe should be updated according to the changes made for MSC30-C (CryptGenRandom() replaced by BCryptGenRandom()).
David Svoboda
Ryan Steele has provided us with a new compliant example using BCryptGenRandom().