Securing your anticheat: Common bad practices, and how powerful are exploiters (+ Guide on Handshakes)

Hello fellow Roblox Developers!

As an anticheat developer, I often saw games that had awful anticheats practices.
This really frustrates me, as a developer, to see beautiful games being ruined by exploiters.

:warning: Warning: This post is for developers that already have a client and server anticheat.

Table of Contents

Common bad practices

1: :no_entry: Using :FireServer or :InvokeServer to signal to the server the client is being tampered with

In the majority of games, I kept seeing in opensource exploit scripts (and even in an AntiCheat from Community Resources!) that the anticheat system used a Simple Ban Remote that once fired, often wipes the player’s progress, and/or just straight up bans the player

It’s a very bad practice as it can just be bypassed by this simple code snippet:

local DummyRemote = Instance.new("RemoteEvent") --> Or RemoteFunction if the anticheat uses a RemoteFunction
local ACRemote = Path["To"].AntiCheat["Remote"]

local OldFireServer; OldFireServer = hookfunction(DummyRemote.FireServer, newcclosure(function(self, ...) --> Or :InvokeServer if the anticheat uses a RemoteFunction
    if rawequal(self, ACRemote) then --> Safely checks if self is the AntiCheat remote
        return --> Return nothing (Doesn't fire the remote)
    end

    return OldFireServer(self, ...)
end))

Instead, use a remotefunction to signal to the server the client is being tampered with!
You should use a remotefunction instead since exploiters won’t be able to spy on it, except on the beta version of synapse, preventing a large number of script developers from spying on the anticheat remote.

You can set it to an OnClientInvoke callback and the server will regularly check that the client isn’t being tampered with:

-- ClientSide
local IsClientTampered = false

-- Blah blah detections
task.spawn(function()
    while true do
        if Blahblah.Something.HelloJeremy and Foo.Bar.ImBroke then
            IsClientTampered = true
        end
    end
end)

MyACRemoteFunction.OnClientInvoke = function()
    return IsClientTampered
end

-- ServerSide, assuming we are in a loop
if MyACRemoteFunction:InvokeClient(MyPlayerSumthing) then
    MyPlayerSumthing:Kick("Stop haxxor!")
end

You could even check if the client isn’t responding after 60 seconds for example, which means the client is trying to bypass the anticheat:

local PlayerData = {}
Players.OnPlayerAdded:Connect(function(MyPlayer)
    PlayerData[MyPlayer] = {
        IsTampered = false,
        LastStarted = os.clock()
    }
    
    while true do
        PlayerData[MyPlayer]["LastStarted"] = os.clock()

        local IsClientTampered = ACRemote:InvokeClient(MyPlayer)
        PlayerData[MyPlayer]["IsTampered"] = IsClientTampered

        task.wait()
    end
end)

Players.PlayerRemoving:Connect(function(MyPlayer)
    PlayerData[MyPlayer] = nil
end)

while true do
    for Player, Data in pairs(PlayerData) do
        if Data.IsTampered then
            Player:Kick("Stop the haxx!!!!!")
        end

        if os.clock() - Data.LastStarted >= 60 then --> 60 being the timeout in seconds
            Player:Kick("Stop yielding the remote you noob")
        end

        task.wait() --> Avoid Script Exhaustion Timeout
    end

    task.wait(2)
end

However, this can be spoofed too, since there isn’t any encryption nor any spoofing checks (see the section about encryption)

2: Making physics checks on the client

This sounds pretty obvious right? To anomic devs it doesn’t

As you can see from this code snippet from V3rmillion, it checks for a constant: NoclipChecking. This shows that the check is on the client.

This is very bad especially for a game as big as Anomic since there will be a 99% chance that a hacker will be in the game flying around in a rainbow avatar…

There are multiple ways to spoof noclip checks, for example just spoofing the position like this (again, very simple example):

local OldIndex; OldIndex = hookmetamethod(game, "__index", newcclosure(function(self, Index)
    if
        OldIndex(LocalPlayer, "Character")
        and OldIndex(LocalPlayer, "Character"):FindFirstChild("HumanoidRootPart")
        and rawequal(self, OldIndex(LocalPlayer, "Character"):FindFirstChild("HumanoidRootPart"))
    then
        -- Puts checks here to check if the calling script is the anticheat one, depends on the anticheat
        return Vector3.zero
    end

    return OldIndex(self, Index)
end))

Now, some of you might be asking why exploiters would return Vector3.zero or any random Vector3 value: Noclip checks usually sets a variable to the old position of the HumanoidRootPart, waits for a set amount of seconds (depending on the anticheat, usually 0.5 or 1 second) and then Raycasts from the Old Position of the HumanoidRootPart to it’s new position. If the raycast function (workspace:Raycast) returns a RaycastResult and it’s Instance field is not equal to nil, it means that the player noclipped.

HOWEVER, if done on the client, to presumably reduce the server workload (which I can understand considering Roblox’s gameserver tickrate is 15 hz), It can be bypassed by just returning Vector3.zero (or a Position where there aren’t any part in it) when an exploiter detects that the AntiCheat is trying to get the Position of the HumanoidRootPart. If the position is being spoofed to Vector3.zero, the check would just raycast from Vector3.zero to Vector3.zero, therefore bypassing the noclip check (since the raycast wouldn’t return anything/or wouldn’t have the Instance field).

3: :no_entry: Using functions such as rawequal in your client anticheat

If not necessary (using this outside of a table that has a metatable), avoid using rawequal, rawget and rawset for detections since exploiters can just spoof the result like this:

local oldrawequal; oldrawequal = hookfunction(rawequal, newcclosure(function(self, SecondObject)
    if (not checkcaller()) then --> Checks if the game called rawequal and not the exploit
        if oldrawequal(SecondObject, SomethingThatCanBeUsedToDetectTheExploits) then
            return false --> Spoofed
        end
    end

    return oldrawequal(self, SecondObject)
end))

or they could just spy on the function:

local oldrawequal; oldrawequal = hookfunction(rawequal, newcclosure(function(...)
    warn("Got arguments:", ...) --> That is detectable lol, but you get the idea

    return oldrawequal(...)
end))

How powerful are exploiters?

This is a question that I saw on the devforums in various forms:

  • Can exploiters modify my script?
  • Can exploiters modify my variables?
  • Can exploiters do …?

and the list goes on.

The answer: it depends on the executor they are using, on the game, their level, and who they are.

Why the executor?

Not all the executors are the same! Most of them have their own custom functions, but the majority follows something called UNC (Unified Naming Convention, which makes exploits that adopt this convention have the same aliases to an exploit function, for example using cloneref on Fluxus would also work on Script-Ware), aswell as different methods of execution, some being better than others.

For example on Synapse, there is this exclusive function called syn.trampoline_call which spoofs the function caller information that is being returned from debug.info (and debug.getinfo exploit-wise, which is a vanilla lua function), such as the source and function to whatever the exploiter desires! Though, it can be detected if not used correctly.

But on an executor such as Celery, which is a basic executor, you won’t find such complex functions.

Exploiters can hook functions and can set a function’s upvalues and constants (for example bypassing a cooldown on the client by setting a number constant from 10 seconds to 0)

Why the game?

Some games are more vulnerable than others, and have different mechanics and goals

For example a game like Arsenal, you will be powerful by having an Aimbot and ESP Script,
But for a game like Pet Simulator X, you will be powerful by having an autofarm.

Why the level and who they are?

It depends on these two because usually a consumer (a user that runs a script they bought/got from a youtube video) is usually someone with no experience in exploiting and are more likely to be detected by the ingame anticheat (if there is one) or just having trouble making their own script for a game. But on the other hand, a script developer will have experience in exploiting and most likely will bypass anticheat measures, some being harder to bypass than others (depending on the game).

[BONUS] Handshakes and Encryption

This is probably why you read this post in the first place: the guide on how to make a handshake.

A handshake in anticheats often is basically the server checking if the client anticheat is working, and optionally passing in the handshake what detection got triggered.

A simple handshake with a basic encryption system is typically is the following:

-- ClientSide
local MyCipherFunction = function(String) --> Could be any kind of encryption, such as AES, or even a homemade encryption system, you shouldn't use this though, as this is just a placeholder of what it could be
    local Characters = {string.byte(String, 1, #String)}
    local CipheredCharacters = {}

    for Index, Character in pairs(Characters) do
        CipheredCharacters[Index] = Character + 1
    end

    return string.char(unpack(CipheredCharacters))
end

ACRemote.OnClientInvoke = function(StringToCipher)
    return MyCipherFunction(HttpService:JSONEncode({
        TamperedWith = IsBeingTamperedWith,
        CipheredResult = MyCipherFunction(StringToCipher)
    }))
end

-- ServerSide, in a PlayerAdded
-- This of course should have error handling (pcall, checking if response is decipherable, if its valid json, etc) and such, this is just an example
while true do
    local Success = pcall(function()
        local OriginalString = tostring(math.random())
        local Response = HttpService:JSONDecode(MyDecipherFunction(ACRemote:InvokeClient(Player, OriginalString)))

        if Response.TamperedWith then
            Player:Kick("Client has been tampered with")
            break
        end

        if OriginalString ~= MyDecipherFunction(Response.CipheredResult) then
            Player:Kick("Ciphering has been tampered with (Possible Handshake Replay Attack?)") --> Stops exploiter from returning the same thing everytime
            break
        end
    end)

    if not Success then
        Player:Kick("Tampered with the handshake in an attempt to bypass the client anticheat")
        break
    end

    task.wait(5) --> Wait 5 seconds before next handshake
end

This is a basic example and you can add any check you want! For example: returning a multiple of pi to the power of os.clock()

The end

Thank you for reading my first community tutorial on the devforum!
Let me know if you have any issues!

And if I made any grammar mistakes please let me know, since I made this at 3 AM :skull:

You should also take a look at this tutorial by my friend @CodedJer, in which he explains in simple terms how to make a good base for your client anticheat in addition to your server one

And you should also take a look at my replies on this anticheat topic as it also shows in a real world scenario how exploiters could bypass your anticheat

50 Likes

Thank you so much! Very detailed and super helpful for the situation I’m in right now!

12 Likes

No problem! Let me know if you have any issues.

8 Likes

is this going to stop all hekers in my games?

With all seriousness though this is a really nice looking thread.

9 Likes

Yes its going to stop the sussy hekers using infinite yield in your game aswell as shrimple spy.

Thank you sinkZ!

5 Likes

This seems like a lot of unnecessary things that you wouldn’t need if you just write good code that prevents the client from doing anything it’s not supposed to do.

6 Likes

The handshake is to check if they tried deleting the script or to yield it (not responding to the handshake means it was yielded)

Also what you just defined was an anticheat, which is the topic of this tutorial

4 Likes

I meant these extra measures should not be needed.

Them deleting client scripts should only affect them. If them deleting any local scripts affects your game for other players then your game would need more than an anti-cheat.

I’m just saying, relying on an “anti-cheat” to secure your game isn’t optimal or good practice.

4 Likes

Them deleting the anticheat should be detected as it could ruin other people’s experience

So roblox adding byfron isn’t good practice?

3 Likes

I was specifically talking about per-game, not roblox’s anti-cheat. Thats different.

Also, your handshake method is a bit flawed because if someone is lagging they will get falsely kicked (it also contains memory leaks)

your anti-cheat should be server-sided

4 Likes

Your anticheat should be both client and server.

My handshake was a DEMONSTRATION to what a handshake looks like.

If a player lags, don’t worry, its a timeout of 60 seconds, which means the player must be freezing for 60 seconds,

Except Roblox would NORMALLY have kicked them earlier from failed ping requests

EDIT: I hate my mobile keyboard

4 Likes

I’m just saying, relying on an “anti-cheat” to secure your game isn’t optimal or good practice.

What are you supposed to do? Allow people to freely exploit your game with no restrictions?

5 Likes

This could yield the server forever.

3 Likes

This is why I added checks in place in the second example

if os.clock() - Data.LastStarted >= 60 then --> 60 being the timeout in seconds
    Player:Kick("Stop yielding the remote you noob")
end

I even added a warning there

3 Likes

Does kicking actually unyield the server?

2 Likes

It’ll throw an error along the lines of “Tried invoke but client left”, and won’t affect the server since it’s in a protected call, so you don’t have to worry about that!

Kicking doesn’t unyield

3 Likes

изображение
Is there supposed to be ACRemote.OnClientInvoke instead of ACRemote.OnServerInvoke, because that doesn’t seem right.

3 Likes

my bad, let me fix that real quick

edit: done

1 Like

good resource here, but MyDecipherFunction does not exist

1 Like

It was an example of a cipher and decipher, it’s not code samples that you can put directly in your games. It just reverses the cipher function

2 Likes