Your RNG Logic Is Flawed

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.randomseed

Takes 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:

  1. C# Version

    • 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.For to 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).
  2. 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.

20 Likes

This really does put it into perspective. Brute forcing 4.3 billion seeds is lightwork for any CPU developed in the past 5 years. If I ever needed a secure and unpredictable RNG system I would use cryptography or something similar! Thanks for bringing this up.

4 Likes

Extemely interesting information. Never knew about this. Really appreciate you sharing this topic/potential vulnerability

1 Like

I’m assuming that random logic is calculated server-side.

Also, if multiple players share the same server, you can’t determine the ordinal rank of each random number.

For example: the server calls math.random for my roll and it returns 47, then you roll and get 59, then you roll a second time and get 31. If I rolled all three times, I’d get the same 47, 59, then 31.

No?

1 Like

Yes

In the place I linked, you can.

1 Like

I had to look up what XSH-RR was but it was what I thought it was, for those wondering, its another name for PCG (which is what they call it on the compat page and in the source code)

I know someone brought this up, but if the attack requires consecutive bytes, surely entropy would make it harder to crack said seed, math.random uses a singular seed.

In the Luau sourcecode that initialises this seed value, the random seed is indeed a 64 bit int, but because Luau ints are only 53 bits (32 for randomseed), I see the attack vector otherwise.

Even if it was 64-bit (seed hasn’t been changed) it’s extremely insecure anyways. A cluster of bots can trivially crack said seed.

Re-seeding does make it harder to crack, but it’s objectively better to use a CSPRNG like ones based off ChaCha20 if you’re that determined to prevent attacks. :slight_smile:

I’ll clarify that XSH-RR is the algorithm behind math.random for folks who don’t know what PRNG stands for. Thanks!

What do you mean by “entropy”? The “complex” formulas I’ve shown and disproved in this topic, or more generally how the seed is generated? Something else?

Both are pointless as previously mentioned because the search space doesn’t change (and also because the PCG family of PRNGs aren’t meant to resist brute forcing attacks).

I think that the conclusion is wrong, can’t you just throw a few math.random() calls into nowhere on the server once in a while? Wouldn’t that make brute forcing impossible? Unless you actually need seeded output, that should be the solution.

1 Like

That’s indeed a solution if you don’t want to use a CSPRNG as I mentioned in my previous topic:

That said, this is not cryptographically resistant. A determined attacker can still simulate and crack it, because the underlying PRNG is predictable.

That’s why CSPRNGs exist. With a CSPRNG and a secure seed, things like random advancement are unnecessary. You get security “for free” with guarantees that hold up against brute force by today’s standards.

This conclusion has been tested for decades by cryptographers and has consistently held up under scrutiny.

Hope that clears up the misconception!

It is not uncrackable, but that’s more than enough protection for a typical roblox game. It’s not like anyone will spend weeks trying to figure out how to get the seed for a random cashgrab

That argument only holds if you assume nobody will ever bother. The reality is that recovering the state of XSH-RR does not take weeks, it can be done efficiently once outputs are observed. Saying “good enough” creates a false sense of security. If obfuscation were sufficient, the entire field of cryptography would not exist.

1 Like