PSA: Why You Should NEVER Use math.random for RNG-based rewards

Why You Should Never Use math.random for RNG-based rewards

Nice to see you folks again. This topic will be extremely valuable to you if you plan on adding RNG-based lootboxes, or anything that rewards based on math.random or Random.

Background

I got the idea to write this topic because my friend @XoifailTheGod sent me this YouTube video of a seemingly skilled developer using Roblox’s pseudo random number generation to their advantage.

The experience affected by this vulnerability was the popular experience Rogue Lineage.

NOTE: I DO NOT condone the usage of any software or the actions showcased in the video linked above.

Why This Matters

Many developers assume that math.random() or even Random.new() provides “good enough” randomness. Unfortunately, these functions are deterministic, and when not used carefully, their output can be predicted or even replayed by attackers, especially when used for:

  • Lootboxes / Rare item drops
  • Combat crits or proc chances
  • Randomized enemy behavior

In the video I mentioned above, the attacker was able to crack the PRNG state and simulated the drop table until a rare item was obtained. Once found, they replicated the same actions in-game and consistently farmed rare drops. This is a direct consequence of deterministic RNG combined with predictable or static usage patterns.

The Root Problem

Roblox uses a variant of the PCG32 XSH-RR algorithm. While its fast and statistically solid for general use, it is not cryptographically secure, and its internal state can be reverse-engineered if:

  • The seed is guessable (e.g., tick() or os.clock())
  • Too few random calls are made between sensitive actions
  • The PRNG usage is tightly tied to game events (e.g., one roll per player action)

In such conditions, a motivated attacker can brute-force the seed or track enough RNG outputs to synchronize with the game’s internal state.


Source: https://www.pcg-random.org/

Don’t Do This

Here’s a naive example of drop logic that looks safe, but isn’t:

math.randomseed(tick())

local function rollLoot()
    local roll = math.random(1, 100)
    if roll == 100 then
        return "Ultra Rare Sword"
    else
        return "Common Item"
    end
end

Problems:

  • Using tick() as a seed is guessable (the attacker only has to try a range of recent timestamps).
  • If the number of calls between seeding and loot is known, the attacker can fully reconstruct the RNG state.

Even worse, if the game never re-seeds, but uses math.random() repeatedly in a predictable pattern, an attacker only needs to match the call count to get in sync.

A Better Approach

To somewhat mitigate predictable RNG behavior, here’s an efficient strategy that works without needing a heavy CSPRNG:

Use Random.new() Objects (Not math.random)

This gives you per-instance PRNGs instead of relying on the global state.

local rng = Random.new()

local function rollLoot()
    local roll = rng:NextInteger(1, 100)
    if roll == 100 then
        return "Ultra Rare Sword"
    else
        return "Common Item"
    end
end

Add Entropy: Random Advancement

Still, using a single Random.new() predictably can still lead to abuse. So we introduce entropy via random advancement:

local RunService = game:GetService("RunService")
local rng = Random.new()

RunService.Heartbeat:Connect(function()
    for _ = 1, math.random(1, 5) do
        rng:NextInteger(1, 10)
    end
end)

Now the number of PRNG calls between loot rolls becomes harder to predict, which makes it much harder to sync with or reverse.


Vary the Number of PRNG Calls

Instead of calling Random::NextInteger() once per action, try this instead:

for _ = 1, rng:NextInteger(1, 3) do
	rng:NextNumber()
end

Randomizing the number of RNG calls between events forces attackers to guess exponentially many possible PRNG states, making prediction or syncing practically infeasible.

Should I Use a CSPRNG?

In case you’re unfamiliar, a CSPRNG is a cryptographically-safe pseudo random number generator. It’s used in security-sensitive applications, where security is a top priority.

To get back to the question, from a technical standpoint, yes, but practically, probably not.

Using true cryptographically secure randomness like SHA-based output or ChaCha20 would make prediction computationally infeasible, and close to impossible by today’s standards, but with a cost:

  • Slower performance, especially on client devices
  • No native support in Roblox; you’d need custom hashing libs (like rbx-cryptography, hello @daily3014TheGod :slightly_smiling_face:)
  • Increased implementation complexity

Instead, you can get “good enough” unpredictability using Random.new() by amplifying its entropy.

Bonus

If you are still lost, you can take a look at this template of a “good enough” setup of a Random pool with random advancements (we essentially use lag to our advantage, which is a low entropy amplifier).

local RunService = game:GetService("RunService")
local rngs = {Random.new(), Random.new(), Random.new()}

RunService.Heartbeat:Connect(function()
    for _, rng in rngs do
        if math.random() < 0.5 then
            rng:NextInteger(1, 100)
        end
    end
end)

local function rollLoot()
    local rng = rngs[math.random(1, #rngs)]
    local roll = rng:NextInteger(1, 100)
    return (roll == 100) and "Ultra Rare Sword" or "Common Item"
end

Final Thoughts

This topic might seem obscure now, but as games become more competitive and cheaters more sophisticated, RNG manipulation will become a bigger threat. Fixing this today protects your game tomorrow, and your players will thank you for it! (good economy = players happy)

If you’re developing anticheat or secure game logic, feel free to reach out. This kind of unpredictability is something we take seriously in projects like OneAnticheat :slight_smile:.

See you in the next post!

Credits to @Unlimited_Objects for existing, and @daily3014TheGod for making rbx-cryptography!

68 Likes

Brilliant topic, thank you. I watched the mentioned video earlier this week and was blown away on how much can be achieved with such a small looking oversight. Give people bits, and they will create a masterpiece I suppose.

5 Likes

…or give them classes, and they will create memory leaks

2 Likes

This was a good quick read, I dont normally set my random seed, but I guess thats a similar issue lol. If you just get more next numbers with a random next number between 1 - 3 is that not a pattern they can find? I think if they go to this length to find something like that they will try to find the pattern.

2 Likes

If you’re talking about the “Vary the Number of PRNG Calls” part, then no, it’s still really hard for someone to figure out the pattern. Because the number of calls changes randomly each time (and most importantly its results are not shown to the client), it makes guessing the exact state of the random generator much more difficult. This means attackers would have to try many, many possibilities, which takes a lot of time and effort.

1 Like

Added CSPRNG to cryptlib: rbx-cryptography/src/Verification/EdDSA/CSPRNG/init.luau at main · daily3014/rbx-cryptography · GitHub

6 Likes

Okay, so you are saying its much harder because they would have to know the exact state it was in to know the new state?

2 Likes

Close but no, I’m saying that to bruteforce the state that the PRNG is in, you’d need to know the previous numbers it generated as well. Not showing those numbers to the client makes it practically infeasible to crack the PRNG.

3 Likes

Oh nicee I understand, so just a few invisible randoms make it hard to predict. ill def change some of my random code!
Thank you.

2 Likes

Good thing my classes are taped really well, they don’t leak.

3 Likes

If Random is better to prevent prediction then that should be stated on the documentation for math.

Disclosing the algorithm used to achieve number generation is also pretty helpful to people trying to manipulate it, unless it could be obtained anyway.

3 Likes

big lootbox doesn’t want this trick known; get 'em liker

3 Likes

The PCG32 XSH-RR algorithm is open-sourced and licensed under the Apache 2.0 license which requires attribution either way, so even if Luau was closed-source, Roblox would still have to credit them.

3 Likes

Vouch for contribution! Loved the details of the project, as well as this write up :smiling_face:

2 Likes

Amazing post liker. Much love! :heart:

2 Likes

This reminds me, in mid 2024 I use to abuse RNG manipulation in a lot of games, notably Oaklands. It’s actually quite an epidemic how unsecure most Roblox games are with their RNG management.

PCG32 itself is easy to break, the hard part most of the time is just getting access to the random numbers generated by PCG23.

3 Likes

Great read, thanks for making this Liker!

1 Like