Clientside Anti-Tamper Solution using a Car

Yo!

In this code, we use the same security a Car uses to protect its car keys for generating dynamic and secure random numbers in Roblox, which can be particularly useful for applications like Anti-Tampers and Anti-Cheats.

What are Rolling Codes?

Rolling codes are a type of security mechanism used in various systems, such as garage door openers, keyless entry systems, and remote controls. They rely on a constantly changing code that is synchronized between the transmitter (e.g., a remote control) and the receiver (e.g., a garage door opener). Each time a new code is generated, it becomes the next valid code in the sequence, preventing replay attacks and ensuring that the same code is never used twice.

Generating Dynamic Random Numbers

To implement rolling codes or similar security mechanisms in Roblox, we need a way to generate random numbers that change over time in a predictable and synchronized manner that cannot be replicated by an attacker. Here’s an example image on what the code does

-- ReplicatedFirst.Client
local salt = "Hello, World!"
local bytesOfString = 0

for i = 1, #salt do
	local byte = string.byte(salt, i)
	bytesOfString = bytesOfString + byte
end
--H (72) + e (101) + l (108) + l (108) + o (111) + , (44) +   (32) + W (87) + o (111) + r (114) + l (108) + d (100) + ! (33) = 1191

local increment = 0

while wait(1) do
	increment+=1
	local unixTime = os.time()
	local minutesSinceEpoch = math.floor(unixTime / 60)

	local randomSeed = minutesSinceEpoch+increment+bytesOfString
	local random = Random.new(randomSeed)

	local randomNumber = random:NextNumber() -- Random number between 0 and 1

	remote:FireServer(randomNumber)
end


Using the Random Number Generator

We can use this code to perform a handshake between the client and the server. This handshake is useful to determine if the client has been tampered in any way and if a remote doesn’t come within a specified period of time, kick the player.

-- ServerScriptService.Server

local Remote = game.ReplicatedStorage.RemoteEvent
local salt = "Hello, World!"
local bytesOfString = 0

for i = 1, #salt do
	local byte = string.byte(salt, i)
	bytesOfString = bytesOfString + byte
end

game.Players.PlayerAdded:Connect(function(player)
	local increment = 0
	local responseWaitTime = 10
	local lastRespondedTime = 0
	local playerDisconnected = false
	
	local connection = game.Players.PlayerRemoving:Connect(function(player)
		playerDisconnected = true
	end)
	
	local connection2 = Remote.OnServerEvent:Connect(function(player, playerNumber)
		increment += 1
		local unixTime = os.time()
		local minutesSinceEpoch = math.floor(unixTime / 60)

		local randomSeed = minutesSinceEpoch + increment + bytesOfString
		local random = Random.new(randomSeed)

		local randomNumber = random:NextNumber() -- Random number between 0 and 1
		print("Servers random number: " .. randomNumber)
		print("Player " .. player.Name .. "'s sent number: " .. playerNumber)
		print("Match: " .. tostring(randomNumber == playerNumber))
		if randomNumber==playerNumber then
			lastRespondedTime = 0
		end
	end)
	
	repeat
		task.wait(1)
		lastRespondedTime +=1
	until playerDisconnected or lastRespondedTime>responseWaitTime
		connection:Disconnect()
	connection2:Disconnect()
	if not playerDisconnected then
		player:Kick("")
	end
end)

Securing the client-side

Well, obviously. If you have the code, the key, and the algorithm, you could just remove the parts of the code that do stuff like anti-cheats, and just have the handshake. This can be prevented by obfuscating the client script.

You can obfuscate LUA scripts with this website:
LuaObfuscator - Playground

Using obfuscation, the attacker won’t have any knowledge of the insides of the contents, leaving everything safe.

Full Code:
RollingCodes.rbxm (3.4 KB)

Feedback is greatly appreciated!

14 Likes

Rolling codes are a good idea for anti cheats, but I wouldn’t recommend using time-based codes. Players with high ping or lag could cause the client to desync. Instead add some more logic to to the remote call increment. For example, when a player first joins, give them a starter value like 1234 and The client then adds 1 to this value with each remote call. You can add more complexity by changing the increment based on conditions, such as adding 12 instead of 1 if the number is prime.

Pair this with server side integrity checks like asking the client to return the hashed version of the current number to stop any exploiter replicating remote calls. Also, ‘encrypt’ the number with a custom complex math algorithm, using bit32 library or similar operations to transform the number, then run the same operation on the server and check they match, something like this.

local function Transform(Number)
    local Storage = Number
    Storage = 2^32 - bit32.bxor(Number, bit32.bnot(2^31, Number))
    Storage = bit32.lrotate(Storage, 10 - bit32.countlz(Storage))
    Storage = bit32.bxor(2^24, Storage)
    return Storage
end
5 Likes

Huh, never thought about desync! Maybe I could have like a “resynchronization window” where the receiver will accept codes that are a certain number of increments ahead of its current counter value, but if the transmitter gets too far ahead and outside this window, it will be rejected.

3 Likes

While in theory you can do something along those lines issues might happen if instead of the client the server lags behind. I think using entirely custom methods are more secure as functions like random.new() and os.time() are easily accessible to exploiters. Whereas if you have custom logic its a lot harder to figure out for an exploiter especially with obfuscation and the fact that decompiling scripts doesn’t give a perfect recreation of the script.

5 Likes

Fixed desync between client and server.

3 Likes

While this sounds good on paper, this is no different than a normal “random” key system when looking at client security. An exploiter can easily hook Random.new to cache the most recent random object being used on the client, giving them access to send their own keys with NextNumber() too.

Even if obfuscated, which already is a bad practice considering the performance decreases, that still doesn’t prevent an exploiter from doing the same as above. Plus, since this system is time based, players with long lagspikes or unstable connections will eventually run into issues.

I wouldn’t go out of my way to add something like this, but rather work on securing your properly remote events on the server so they cant be exploited as much. PoC Bypass:

local acRandom
local old;
old = hookfunction(getrenv().Random.new, function(...)
     local ret = old(...);
     acRandom = ret;
     return ret;
end);

while wait(1) do
     print("anticheat random is", acRandom);
end

3 Likes

Roblox is so terrible at countering hookfunctions bruh. Guess I’ll have to make my own psuedo-random generator in pure-lua.

2 Likes

With the help of the God of Scripting ChatGPT 3.5. A new psuedo-random generater without hookfunction bull crap!

local m = 2^32 
local a = 1103515245
local c = 12345 

local unixTime = os.time()
local minutesSinceEpoch = math.floor(unixTime / 60)

local seed = minutesSinceEpoch 

local function randNew()
	seed = (a * seed + c) % m
	return seed / m
end

local i = 1

while true do
	wait(1)
	local randomNumber = lcg()
	print(i, randomNumber)
	i = i + 1
end

hookfunction os.time or math.floor the handshake fails and the server kicks you

3 Likes

Benchmark Stats:
0.00002060001133941114 seconds Client Side every calculation
0.000017900019884109497 seconds Server Side every calculation

2 Likes

Added a patch that changes the time window for a player to send a handshake and a “resynchronization window” where the receiver will accept codes that are a certain number of increments ahead of its current counter value. In the script its 3 codes above are allowed.

2 Likes

Do you know if we are allowed to use obfuscated code in our games? I remember reading something a long time ago that suggested we are not

2 Likes

Yep! As long as you don’t publish it on the marketplace it should be fine.

2 Likes

i see an instant problem with this. theres something called remote spy that can just look at the arguments passed into the remote. then they could just take that those arguments which has the key and call the remote
they can do it however many times as they want in a minute

if all this is doing is telling the server this one script is running, then why would a hacker mess with it. if its included in another script then the method i said above would ignore it

The key isn’t sent through the remote, the random number is that is generated by the key is sent. Which means the hacker cant send the arguments because they don’t know the key that generates it since it’s obfuscated.

This is cool and all, but once the defense is penetrated, you are left with an ugly script running all of this on the background, that is useless.

I mean if the person figures out the key the developer can just change it and re-obfuscate the script. :person_shrugging:

1 Like

yeah but what if they just make an algorithm to just de-obfuscate your script.

Any obfuscation is basically impossible to turn back into regular Lua bytecode without manual de-obfuscation. If this happens you could use a better obfuscation method than luaobfuscator and use Luraph, Ironbrew, or Moonsec v3.

A bounty on IronBrew 2 was placed for around 500 dollars yet no one was able to win it for the sole reason of it being hard to “deobfuscate”.

A general rule of thumb for breaking security is breaking the implementation instead of the encryption.

1 Like

Just compile the obfuscated code and then decompile easy :person_shrugging: (its not LOL)

if both the server and client use the same seed for random, and random:NextNumber will return the same thing with the same seed. since you posted this code publicly (the math is public), there may be a way to get the key by reversing the random process

os.time() is public so thats easy to get, with remote spy they can get the randomly generated number. they have 2 of the 3 numbers required for the seed, with the 3rd being the key and they have the number generated. if they know how Random works it may be possible to reverse engineer the seed then therefore the key

so it could be possible to still get the key