How you should secure your game - A beginner guide for secure networking and developing anticheats

This is a super long thread so read it at your own pace. :smile:

How you should secure your game

You may have seen the “never trust the client” phrase tossed around if you’ve read or created scripting support threads about remotes or game security. I’m going to explain this phrase and give some useful information about remote and client security as well as give some information about effective anticheats.

Never trust the client?

Lua and bytecode

Most developers when they first start learning about game security will ask, “Why can’t exploiters just be stopped with a script on the client?” Well think about it like this. The server has to send some kind of code to the client so that the client can run this code. That means this code must be stored somewhere for the client to use it. And that code is stored in the client’s memory. This “code” isn’t like the lua code that you write though. It’s a version of the lua code called “bytecode” which the client can interpret. When you write your game’s lua code it will be compiled when the game starts and each LocalScript’s (and ModuleScript’s!) bytecode will be sent to the client.

Decompilers

This bytecode and the values and functions the bytecode accesses can be changed at any time. The fact that this code is stored in memory and an exploiter has access to it also means it can be “decompiled.” A decompiler on a basic level just tries to create lua code which produces the same bytecode when compiled. That means any information which is not included, like local variable names and comments also cannot be decompiled. This absent information would make the decompiled code much easier for an exploiter to read. Keep in mind, since the bytecode for server scripts (and module scripts located in ServerStorage and ServerScriptService) are not sent to the client, they can’t be decompiled!

Never trust the client!

An exploiter has access to the client side code in your game and can change all of it’s functionality, whether that be editing the script somehow, or changing the values and functions it uses. That means any client side checks you place on the client can just be removed by an exploiter and the values it checks can also be changed! Any functions you call on an instance or from the global libraries like math and table can be changed by the exploiter without you ever knowing. That means they can even intercept the data that your client receives from or sends to the server. All remote traffic from and to your client is visible to the exploiter!

Networking your game

What code should you run on the client?

The client should always “pretend” like it’s right. You can think of the client like a mini version of the server that the player has control over. You want the client to mimic your server code by itself and then notify the server that it performed the action. The server must verify that this action is allowed using the same code the client does.

Sometimes you need to resync the client such as for placing an invalid object. If the client places an item they don’t have they should see it before the server tells them to remove it. This is a sign that you’re doing it right even if it seems like that’s wrong. For example, input events should be handled on the client and when an action is done on the client, like an attack, the client should ask the server to also perform the attack action. The server will verify and clamp the request before it performs it.

What code should you run on the server?

Obviously any server-side objects should run code on the server. In this case the server is like the client without any user input. The server is also the “authority” meaning anything the server says to other clients should always be your definition of correct. You always want to avoid a client making something invalid on the server.

The server should always verify the client’s requests. This is usually as easy as simply sharing some server and client code and having the server mimick what it thinks the client should be doing.

Here’s some pseudo code for example:

-- Server
local function onAttack(player, target)
    if target.Team ~= player.Team and inRange(player, target) then
        attack(target)
    end
end

-- Client
local function onAttack(target)
    playAnimation()
    if target.Team ~= localPlayer.Team and inRange(localPlayer, target) then
        fireServer(target)
    end
end

Don’t send too much data!

You should limit yourself to one basic remote request per frame for something like ping. Don’t send a lot of data per frame! If you are sending a complex table which needs to be updated you should send the initial table once and then update each value in the table individually only when the value changes! You always want to make sure you aren’t sending too much information to the client since this can slowly increase a client’s ping to several seconds! Don’t worry about sending too much data once as long as it means you aren’t sending it anymore after. Treat all of your remote requests like they are one big request.

Example:

-- Good
SyncRemote:FireClient("syncSettings", settings)
local function syncHandler(self, index, value)
    SyncRemote:FireClient("syncSetting", index, value) -- We only send one property at a time and only when it changes! Much less perceived server lag!
end
local settings = setmetatable({}, {__index = settings, __newindex = syncHandler})

-- Bad
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function()
    SyncRemote:FireClient("syncSettings", settings) -- Lots of information might be sent very quickly!
end)

Anticheats

Types of anticheats

Personally I classify anticheats into two main categories. Passive anticheats, and aggressive anticheats. Which one you choose is up to your preferences.

Passive anticheats

A passive anticheat is an anticheat which prevents a player from doing unwanted behaviors. This is my personal preference because it is usually more accurate and results in an overall better user experience if done correctly. These anticheats are hard if you’re a beginner.

Examples of a passive style anticheat:
If a player moves too fast or teleports without the server move them back to their previous position. (I usually just set their previous position when I need to teleport them on the server)
If a player is floating move them to the ground.
If a player noclips, move them back to their previous position.

-- No teleporting or speeding from the server side!
local leeway = 2 -- How many studs of leeway can the player have before they are stopped? Low values = more rubber banding during lag!
local player = aPlayer
local RunService = game:GetService("RunService")
local lastPosition
RunService.Stepped:Connect(function(deltaTime)
    local character = player.Character
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if character and character.PrimaryPart and humanoid then
        local maxSpeed = math.max(humanoid.WalkSpeed, humanoid.JumpPower) + leeway
        character.PrimaryPart.Velocity = character.PrimaryPart.Velocity.Unit * (math.min(maxSpeed, character.PrimaryPart.Velocity.Magnitude)) -- Limit their velocity!
        local currentPosition = character:GetPrimaryPartCFrame()
        if lastPosition then
            local deltaPosition = currentPosition.p-lastPosition.p
            if deltaPosition > character.PrimaryPart.Velocity*deltaTime + leeway then -- Check if they are moving faster than their velocity!
                character:SetPrimaryPartCFrame(lastPosition) -- Reset their position!
            end
        end
        lastPosition = currentPosition
    else
        lastPosition = nil
    end
end)

Aggressive anticheats

Aggressive anticheats punish the player for misbehaving. For example, if the player makes an unauthorized request that they can’t make on their own such as using another player’s items/structures, or attacking with a weapon they don’t have the player can be kicked or banned. This can also include detecting cheating.

Permanently banning a player is a huge risk for not a lot of effect. If a player is exploiting they are most likely using an alternate account and will just create a new one.

Honeypots

Honeypots are used to trick exploiters into firing a fake remote. For example, if your game has currency you can have a fake remote called “AddMoney”. Under normal circumstances having a real remote like this is an extremely bad idea. But it happens and that’s reason to turn it into a Honeypot! When the remote is fired you know that it was fired by an exploiter.

Other anticheat information

When developing anticheats you should remember that you can have false positives. You should always make sure you won’t invalidly punish a player even if it means reducing the strength of the anticheat. You can also combine both anticheats to get the best of both worlds. One example of this might literally be using both forms of anticheat, or it could be making it annoying for the player to play if they’re cheating without effecting them negatively. For example, instead of banning or kicking the player you can temporarily remove their ability to purchase items or interact with certain things. You can even disguise this as a glitch such as giving them a fake item named “Invalid Item” or something and maybe they’ll complain :smile:. Make it fun!

114 Likes

Absolutely brilliant topic, well written and easy to follow. I’ll be making sure to share this around.

Since the client only recieves the bytecode, does this also make practices like obfuscation unnecessary?

16 Likes

Thank you! :smile: Also that’s a good question! It honestly depends… Minification is unecessary (renaming variables to the most basic thing they can be and removing whitespace) since it’s basically already done by the compiler, but if the obfuscator isn’t just minifying code and it’s changing the structure of the code then that structure will be maintained in the bytecode.

If the obfuscator is making strings harder to read somehow than it may help a little bit but at the end of the day you shouldn’t really rely on an obfuscator to keep your code safe since there are other methods of figuring out how these scripts actually work (such as looking at the content of variables and hooking function calls).

The only time I think an obfuscator might be useful is when sharing code you want to keep secure legally… People can figure out how it works but that doesn’t mean they can recover the original code you worked with or modify it.

5 Likes

In my opinion, I really don’t find obfuscators that much use at all. Sure, they can slow down the process of finding the actual content behind the obfuscation, but commit half an hour to using Ctrl + F and replacing stuff and you land yourself with the fully functional script.

If the script is not properly debugged, obfuscation methods can also make debugging 5000% more miserable since you have no clear cause of the issue without having to go through the process of deobfuscation yourself.

6 Likes

I feel like some of these resource threads and discussions are often heavily imbalanced: you have people who talk about Lua, then those who just talk about the C-side and technical details. There’s no real middle ground and I find following conversation a muddled process. I appreciate that you took the time to summarise some of those concepts when addressing them rather than assuming readers have that kind of knowledge off the bat.

I had a read through it and it’s definitely got a beginner feel to it. I don’t quite know about intermediate though, seems to be more like a refresher and fact check sheet, but overall I don’t mind. TIL categories of anticheats (no seriously, before I didn’t look into it and I didn’t care much either).

Thanks for the information.

2 Likes

You should mention honeypots.

A good way to catch loads of exploiters is to make lots of different fake events and then you know when someone is an exploiter if they fire one of them.

Another way to honey pot is if any parameter passed to a remote is missing or wrong but wouldn’t be if it was a real request then you know that it’s an exploiter.

8 Likes

Yeah, I guess it depends on your idea of intermediate. I tried to keep the explanations simpler so that it would be easier for beginners to understand it while getting a lot of the information across. Also those anticheat categories are more of my own classification I guess. I’m glad I taught you something though! :smile:

@grilme99 That’s a good thing to mention. I was sort of trying to include the second part of that but I guess I didn’t get to the first part.

1 Like

Would you consider returning to this thread with some examples? I feel like you and Colbert both have the right of it but I’m not worried about what I should reply when someone asks me - are you beginner or intermediate? - I think it’s more important to know whether or not I understand what’s being discussed or looking at a subject that I already know about in a new way.

Your example about how to consider using tables is really interesting and relevant. I’d love to see more step by step examples of that if you were whiling to dive deeper into the example.

Either way - thanks!

2 Likes

The classic “loopkillAll” and “banUser” remotes, all stuffed in a folder conveniently titled “Admin”.

It works every time, lol.

8 Likes

I have never understood how checks are triggered. Is it a loop on the server, constantly checking? That is the only way it would make sense to me.

2 Likes

There are many ways to do it but often you’ll see people looping through all the players/characters for example, checking for odd differences in speed/height. As-well people usually handle some of this inside their remotes to check for false info sent from the clients, this is usually indicative of an exploiter trying to fire a remote manually. Thats kinda the basis for how the checks work if this is what you were referring to.

3 Likes

The problem is, they always come with new alt-acc’s back. Try to find a way to distract their interest on your game. Pain them passively with things like Data-Bombing that raises their pings and fps, so (artificial) lagging occurs.
No one likes (massive client-)lagging games. If they decide themself to leave, they don’t come back.^^

I’ve updated this article! I’ve added a few code examples, honey pots (as suggested by @grilme99), and rephrased a few things slightly just to make them clearer.

Also I apologize for SPACES but I typed this on mobile. I’ll make sure to update it when I can.

That wouldn’t do anything to stop exploiters. All they have to do is install a VPN.

1 Like

IP bans have been discussed previously and personally I disagree with this. An IP address is only linked to the router. On public WiFi, or shared WiFi you could be banning potentially hundreds of people. By accident. Also, IP addresses can actually be changed relatively easily. Some routers allow you to do this in their settings I think (although I’m not positive) and I know for a fact you can simply contact your ISP. That’s mostly a way for people to stop DDOS attacks.

2 Likes

So if I’m understanding this correctly, the fact a local variable cannot be decompiled makes it easier for the exploiter? Sorry I’m not quite understanding this sentence, could you elaborate?

No, Luau removes variable name information (or “debug” data as I think It’s called?).

If an exploiter attempts to decompile a local script, they can still see all the values, but not the variable names.

It makes it slightly harder for exploiters to know the names of your variables, but It’s still possible to figure out what they’re doing.

1 Like

Sorry, thank you for pointing that out! I’ll reword that.

I’ve updated the post with this:

That means any information which is not included, like local variable names and comments also cannot be decompiled. This absent information would make the decompiled code much easier for an exploiter to read.

So if i had a script:

local x = 5
local y = 10

print(x+y)

What would they see exactly if they were to decompile?

It would depend on the decompiler but they would see something along the lines of this:

local var1 = 5
local var2 = 10

print(var1+var2)

Note: Globals like print, game, workspace, etc along with properties and table functions will still be decompilable since these are either constant (for Globals) or rely on strings (for tables/instances)

1 Like