Client Anti Cheats: Aren't as bad as you think!

Client Anti Cheats

They aren’t as bad as you think!

If you’ve been on the devforum for a while, you’ve probably already seen the saying, “Never Trust the Client”, alongside multiple reasons why you shouldn’t develop a client-sided anti-cheat.

Now, I am not going to say that this claim is 100% false; the client should indeed be considered as a secondary defense, with the server being the main focus for security. However, it is worth noting that some individuals on the DevForums may make such statements without having practical experience in developing anti-exploit measures. I bet that 90% of the DevForums users who make these claims have never actually attempted to create an anti-exploit system themselves.

So the point of this tutorial is to show you how the client-side can be secured. Let’s start with a simple method that prevents the anti-cheat script from being displayed in Dex Explorer and being disabled or deleted.

Environment Hide

A simple method to hide the environment of the script would be as follows:

getfenv().script:Destroy()
getfenv().script = nil

print("Hello World!") --> This will still run!

The following code deletes the script from its environment. This script will no longer show up in Dex Explorer, and it also cannot be accessed in order to disable or destroy it. It is also not visible in getnilinstances()

Handshakes

One of the most common reasons client-side anti-cheats receive criticism is due to how easily they can be disabled or deleted. While the environment hide feature can take care of preventing it from being disabled or deleted via DEX, exploits are still capable of hooking a function that the script uses and causing it to yield. This is where handshakes come into play.

A handshake is basically the client sending a RemoteEvent to the server every few seconds to validate its still alive. If the server doesn’t receive the RemoteEvent within a few seconds, the client will get kicked. Here’s a simple example on how to implement this:

Usually, you would use keys and encryption so that exploiters aren’t just able to replicate the handshake. However, this is a simple example, so it will be a simple FireServer.

--> Clientside
while task.wait(5) do
    Remote:FireServer() --> You would normally use keys and encryption to aovid exploiters just replicating this loop
end

--> Server Side
Remote.OnServerEvent:Connect(function(Player)
    PlayerData[Player].Handshake = tick() --> Reset Handshake Tick
end)

--> In a while loop check PlayerData if the handshake time is over lets say, 30 seconds
if tick() - PlayerData[Player].Handshake > 30 then
    Player:Kick("Communication Timed out") --> The client was most likely disabled or the event was blocked
end

This is a very simple example of a handshake; a more advanced one will, as mentioned already, use keys and encryption to make sure an exploiter won’t just be able to replicate it cough cough aston v1 cough cough

Conclusion

There are many more ways to secure the client via tamper checks, such as Property Spoofing, namecall tampering detections, and more, that I will not go into detail with in this tutorial. This tutorial is mainly supposed to show that the client isn’t just a little script that is helpless and unable to be protected. Client anti-cheats are actually good, especially when combined with server anti-cheats.

Thank you @TheLikerYT for helping me with this post and for allowing me to make the joke about aston version 1

Please excuse me for any grammar mistakes I might’ve made; show me where it is in a PM, and I’ll fix it!

“Never trust DevForum members.”

214 Likes

ASTON V1 on top tho bro :muscle:

Anyways, thank you Jeremy, very cool

image

75 Likes

Thanks for taking the time to write this.

I’m getting the following warning when running this:

Do you know of a workaround for that?

34 Likes

Endorsing the use of getfenv in production Luau codebase is stupid. If you are really desperate for some kind of detection use debug.info.

Using getfenv to try and obfuscate your anti-cheat script will result in nothing more than a hit piece bypassed through getgc and other means.

49 Likes

Hello,

AFAIK, if x uses getfenv but has no connections to y, then only x is affected by the deoptimization (x and y being lua containers)

31 Likes

Put it in a while loop while not script.Parent do

26 Likes

Environments can inherit from each other. What this means is that any inheriting environment will also be deoptimised.

26 Likes

Yes, but my argument was: if x and y are not connected, x’s deoptimization won’t affect y’s optimizations and its inherited environments, correct?

24 Likes

I encourage everyone to please not fight over this or make it personal. It’s been repeated countless of times and doesn’t lead very far. Stay positive and offer constructive criticism.

Rule of thumb that even most AAA games stick to:

  • Focus secure and effective game architecture.
  • Rely on server for validation of data and any secure anti-cheat measures.
  • Deploy some annoying client-sided measures to make the exploiter’s job harder and drive less patient skiddies away. This involves integrity checks, code obfuscation, memory scanning, behavior analysis etc.

Some games choose to hide their scripts in all ways possible, for example, in addition to modifying the environment and destorying the script, some (also) hide key anti-cheats deep in hidden modules etc. Obfuscation. Again, a lot of people will say hidden is neither secure not efficient, but it’s a choice some find effective, and some don’t. It’s a choice based on balancing pros and cons.

As @EtSapientisMagna said, getfenv() normally doesn’t fit with production code. On the other hand, some games I know do find use in hiding their anti-cheats.

Btw, getfenv() remains supported for backwards compatibility, but it’s deprecated and the logging alternative indeed is debug.info(). The new _ENV in higher Lua versions is not supported. Read more here: https://github.com/Roblox/luau/blob/master/rfcs/deprecate-getfenv-setfenv.md.


Apparently there’s a heated discussion about encrypted handshakes. I haven’t seen any popular or effective solutions (yet?), but the problems I see are 1. network limits and 2. exploiter’s full access to all local scripts and replicated modules …

Encryption would sooner help with tampering of a third party than an exploiter tampering with the handshake on a local computer. The stronger the encryption, (most likely) the higher the byte size is. And since exploiters have full access, the logic has to be pretty complicated and dynamic for them to have a hard time replicating it. Lua scripts are not something strongly integrated. Exploiters have a lot of freedom.


I don’t think they’re joking. ReplicatedFirst is an option.

You’re getting this error because the code is running moments after the script got replicated from StarterPlayerScripts into PlayerScripts inside player instance. You can’t reparent in the same frame, so you’ll have to wait a small amount of time before attempting to modify the parent.

42 Likes

My fault, I meant “my question”

Considering you work on deobfuscators (and obfuscators I think?) and make Luau projects that use indeed getfenv, it is pretty weird that you would say this (GitHub - TheGreatSageEqualToHeaven/Fiu: Luau bytecode interpreter, in Luau, uses getfenv in the example, yet intended to work on Luau)

Depends if done correctly, but generally, yes

13 Likes

The luau_load function expects an env table, and the example file gives it getfenv

19 Likes

They are my friend and they told me they were joking, as hiding your script in ReplicatedFirst isn’t, in my opinion, an effective solution

12 Likes

Yes but people could think the use of getfenv is okay considering you used it, as someone with a somewhat advanced lua and luau knowledge

11 Likes

Funny, maybe they were joking, but the fact is, leaving the effectiveness of setfenv() method side, ReplicatedFirst is actually a more suitable option than StarterPlayerScripts, StarterCharacterScripts, StarterGui and StarterPack. All those thingies are containers and elements get replicated (into PlayerScripts, character model in workspace, PlayerGui and Backpack). That means the inactive original copy of “the anti-cheat” remains there.

Otherwise, it technically doesn’t matter where this script is. A strange but working example would be a Script with RunContext set to Client and placed in MaterialService (or any other place).

16 Likes

If cheaters/exploiters use their own dirty methods, we circumvent those dirty methods with our own dirty/hacky measures.
Though I’d like to see the debug.info() version of what you’re trying to argue on (in this case is hiding the script from environment just like with the getfenv() and if its even possible to modify it’s environment)
We’re forced to hide scripts this way, no other way around. :person_shrugging:

20 Likes

Reasons to use getfenv:

  1. It lets you check environments of functions like Instance metamethods, some exploits haven’t patched that in some specific cases making this actually a viable method to check if they were hooked or not.
  2. setfenv can be used to spoof the environment to a metamethod “trap”. this would stop exploiters from indexing the environment of a function they found in getgc(). this can be used together with getfenv() to check the legitimacy of the function caller and to make __index of the metamethod trap redirect back to the real environment after performing some checks.

Also you saying that it shouldn’t be used in production code doesn’t really work as an argument. While it’s true that it deoptimizes the running thread, getfenv would only be used by the client-sided anti exploit and nothing else. Deobfuscation relies on getfenv as it allows indexing the environment as a table instead of having to actually give it away as a constant, I get that it might just be possible to do without by for example keeping a lookup table of every Luau constant but at the same time it doesn’t actually matter unless the anti-exploit in question is extremely slow already.

getfenv is always bad practice but in context of client anti-exploits I think it’s fine to use as long as it doesn’t affect threads outside of it in any way.

18 Likes

(post deleted by author)

8 Likes

This isn’t even the case, the reason why it’s staying is more because of it being used in obfuscation by quite literally every single obfuscator I can think of.

There’s a difference between abusing the fact that you got your hands on an env and just checking it. Don’t know why you completely disregarded that, I am just talking about checking if its legitimate or not.

You can’t just say that without a single argument and expect anyone to take that seriously. Countless of exploiters index environments in getgc to try and get their hands on the script a function is running in, you’d be right if we’re talking about capable exploiters but client anti-exploits aren’t gonna stop those eitherway.

First of all, I am talking about indexing the environment as in for example doing:

for _, Object in getgc() do
    local script = getfenv(Object).script -- indexing the environment can trigger __index here
    -- do something with this script
end

In this example trampoline_call won’t save you unless you’re for some reason trampoline calling your own function. Though a rawget will bypass that, again was just a use case.

Also good job using a really specific exploit function as an example which is actually detectable if not used correctly. trampoline_call does indeed let you set a specific environment which would bypass an env check but any other exploit besides Synapse V3 wouldn’t be able to bypass a blatant index like that.

Why are you so aggressive against the use of getfenv like its slowing the entire game down ten times? The reasons I gave are valid use-cases and will barely slow the anti-exploit down. If you’re trying to stop people from using getfenv so it finally gets removed then you’re not doing a good job because good old luraph is still rocking getfenv in all of its obfuscations. Why doesn’t luraph just stop using it if you can do without?

You can keep saying it’s deprecated and old but it’s not gonna be removed any time soon and it’s a pretty good thing to add to anti-exploits if used properly.

13 Likes

It’s hilarious because I discovered that ‘trampoline_call’ can also be detected, but I won’t reveal the detection and will just say that I performed what needed to be done: Talking about it to certain individuals if you know what I mean.

You constantly use the word “stupid” to describe the acts of other people and are a very contentious person. Instead than reiterating the same thing constantly, why not just call the individual stupid? Oh yeah right, we are on the devforum. We have to be respectful here!

Also you were the one that told me to kill myself…

7 Likes

You seem to really favor them though, with your deobfuscator working for any obfuscator besides luraph. Though that might as well be because it’s just too good, too bad it uses getfenv am I right?

Edit: After some reading it seems like LD was made by people who became Luraph devs shortly after they made LD a thing. Doesn’t really beat my point though.

6 Likes