Your RNG Logic Is Flawed
That’s right, at least, partially.
I’m writing this topic since multiple developers on this forum tend to underestimate attackers when it comes to game security, more specifically with PRNG-related APIs such as math.random, math.randomseed, and Random.
Consider this topic a continuation of my other PRNG-related topic!
Flawed Arguments
“I don’t need a secure output”
Sometimes this may just be true for GFX and other cosmetics, however in most contexts, using that point likely side-steps security, as its usage most likely benefits players.
“Attackers can’t brute-force it easily anyway”
This is only correct if the search space is large enough. With math.randomseed, your seed is capped to a signed 32-bit integer.
In english, that’s about ~4.3 billion possibilities. That sounds large, but even a single modern CPU core can attempt billions of guesses per second.
In Luau, brute forcing it only takes minutes. This makes it trivial for an attacker who knows your seeding logic to replicate your “random” outputs.
“I can just write a complex formula like
tick() + 0x561 / 2 + ...”
Adding complexity to the formula does not increase the entropy!
Both math.randomseed and Random.new(seed) will cast your formula result to a fixed-size signed integer.
For Random.new(seed), the input is cast to a signed 53-bit integer within the interval [-(2^53 - 1), (2^53 - 1)]. No matter how complex your formula looks, the seed value cannot escape those bounds! Attackers only need to brute force within that reduced space.
Actual Bit Security
To put this into perspective…
math.randomseedTakes a signed 32-bit integer as the seed: ~4.3 billion possibilities. Brute forcing takes seconds, minutes in Luau.
Random.new(seed)Takes a signed 53-bit integer: about ~9 quadrillion possibilities. Brute forcing in Luau is unfeasible, but in C with clusters or botnets, it is still possible.
ChaCha20
Takes a 256-bit key: 2^256 possibilities. Brute forcing is practically impossible.
Why Such Mistakes Happen
When you see numbers like 4 billion or 9 quadrillion, it feels enormous, and for everyday gameplay randomness, it can look like “good enough”.
The problem is that computers are designed to handle huge numbers extremely fast. What feels like “uncountable” to a human is often “milliseconds” to a machine.
Attackers do not need to break the laws of physics to brute force your seed, they only need to search the fixed-size space that the PRNG gives them.
Whether you use tick(), os.clock(), or some “complex” formula, the result still gets snapped down to 32 or 53 bits (maximum integer that can fit inside a double).
A Live Example
To demonstrate how trivial it is to recover a weak seed in practice, consider a Roblox experience that generates random cubes of RGB colors on a conveyor.
Each cube outputs its red, green, and blue components separately (in the C# example, in the console, and in the Luau example, directly in the Workspace), and the PRNG is seeded with tick(). In my test, I generate 5 cubes before attempting to recover the seed, giving us 15 numbers to work with!
I implemented two proof-of-concept attacks:
-
- Video here, too big to be uploaded on the DevForum…
- Users input enough RGB values in order (in the video, 15 are inputted).
- The program uses
Parallel.Forto brute-force the 31-bit seed space. - Result: The correct seed is recovered in about 1 second on a modern CPU (Intel Core i5-14600KF).
-
Luau Version (In-Game)
- Video here, too big to be uploaded on the DevForum…
- Place file (the code may not be the best, view at your own risk!): bruteforcer.rbxl (74.7 KB)
- Runs inside Roblox using actors.
- It iterates over all seeds in
[0, 2^31]and checks which one produces the observed sequence of cubes. - Result: The correct seed is recovered in about 1 minute, completely within the game environment, without reading the seed from the console. (check the source out for yourself!)
Both implementations show that even a 31-bit seed derived from tick() is trivial to reverse once a few outputs are known.
This example highlights that relying on tick() or other predictable sources for randomness is UNSAFE if an attacker can observe outputs.
Even what seems like “millions of possibilities” is easily traversed when computers can test billions of seeds per second in parallel.
In Conclusion
If your random output impacts security or fairness, you should not rely on Roblox’s non-cryptographic PRNGs, and instead use a fast and mature cryptography library such as rbx-cryptography developed by @daily3014TheGod and @XoifailTheGod.
Roblox’s PRNG algorithm behind math.random and Random objects (XSH-RR) is perfectly fine for cosmetic effects, noise, or gameplay variety, but not for anything an attacker can profit from.