There are a lot of cheats in GMod, and not a lot of resources that can teach you how to fight them. For the most part, cheaters have no idea what they’re doing, blindly download hacks, and join random DarkRP servers. Unfortunately, not everybody is like that. There is the 1% who know what they’re doing and are able to create their own cheats and circumvent whatever you have in place. I made this in the hopes that it will at least teach people how to make a decent anti cheat. Obviously you can’t stop 100% of all cheating, so please, don’t post saying that this thread is dumb/pointless. I know it’s impossible to stop all cheating, thank you. Another thing I want to add is that I’m not perfect. I don’t know all the rules, I don’t know all the quirks, and I’m no master of Lua. I haven’t even actually created an anti cheat, I’ve just been in the GLua business for a while. If you see something that’s wrong or could be improved, please, tell me so I can fix it. You can even contribute your own stuff and I will add it to the OP.
The point of this thread is to teach you how to stop experienced cheaters: the ones who are above the average skid but aren’t quite yet at the level of being able to say that they are fairly advanced at cheating. Although this guide may teach you to catch even cheaters that are at the top, you’ll probably be catching the medium level cheaters, as I said before. Furthermore, this thread assumes you know how to work with Garry’s Mod. I’m not going to hold your hand and teach you about Lua. With all of that jargon out of the way, let’s get started.
Your first objective when creating an anti cheat is to load first, that is, before any other file has loaded. This step is optional, but I highly recommend it, as it vastly increases the security of your anti cheat.
The very first file to be ran is garrysmod/lua/includes/init.lua. This file is core to GMod and your game would break if it didn’t load, since it loads every other core GLua feature. A nifty little thing you can do is actually replace init.lua with your own version - without actually replacing the file. To do this, you simply mimic its file path in your addon. I’ll name my addon hat-trick, and will use it throughout the rest of this guide.
As you can see in the image, I mirrored the path to the file in my addon. If you were eager enough to launch your server after I just told you about this little trick, you’re probably wondering why your server just shit itself.
Since we overrode the init file, the core functions of GMod never load, and your server goes kaput. Remember, this is a SHARED file, so the server runs it too. To overcome this obstacle, simply all we need to do is copy and paste all the code from the original init file to our init file. Yes, it’s that simple. The only other step you need to do is to add your own code to the bottom of the file. Since you only really need to load first on the client, your code should look like this.
-- The original init file's contents if (CLIENT) then -- Anti cheat stuff for the client to load end
We haven’t yet created a file for the anti cheat to load, so we should probably get on that now. Go ahead and create a folder in the same place as init.lua. In the folder, create a lua file. This is how I’ve done it.
And here’s the init file.
-- The original init file's contents if (CLIENT) then include("hat-trick/hat-trick.lua") end
Congratulations for making it this far. Now, it’s onto the next section.
So we’ve got our anti cheat loaded first before any other addon, but unfortunately, this isn’t enough. There exists methods to load even before this. Although only possible through cheats created in C++, it is still a threat nonetheless. If they’ve loaded before your anti cheat, they can take measures to deceive it. Such is the never ending struggle between cheat and anti cheat. For those that don’t take measures against your anti cheat (aka 99.9% of cheaters), here is your first step to catching them. When a script loads, the amount of memory Lua uses increases. There exists a function, collectgarbage, to retrieve the amount of memory in use by Lua. The function actually serves multiple purposes, but for our objective, we specifically want to use it for this purpose. We can get the amount of memory (in kilobytes) by executing the count command in the function.
local memory = collectgarbage("count")
You’re probably wondering why this is useful, but I bet some of you keen readers are understanding where this is going. Since executing a script increases the amount of memory in use, we can check to see if any foreign scripts have loaded before us. Something as simple as printing the number 1 will change the value returned by the function, although very insignificantly. Take a look at this example.
local memory = collectgarbage("count") -- The original init file's contents if (CLIENT) then include("hat-trick/hat-trick.lua") end
On my server (not client), the value of memory is 711.787109375. This number is, in my experience, consistent. Although I am unsure of this myself, the number for your server or client may change, so I don’t recommend banning someone over a billionth of a byte being higher than what you expected. Another thing you may want to do is actually receive the amount of memory both before and after loading the core init script, or maybe even after loading your anti cheat’s file. Doing this is very simple, but this stuff is entirely up to you.
local memory = collectgarbage("count") -- The original init file's contents memory = collectgarbage("count") - memory if (CLIENT) then include("hat-trick/hat-trick.lua") end
If the memory is higher than expected, you may set a global for your anti cheat to use. How you handle this is entirely up to you. Remember, I did this for my server, not my client. Use proper if statements. I am running the code on the server only as an example. Use proper if statements to make it run only on the client. Don’t check for super tiny decimal places, check for more significant increases in memory. As a reference, the original value for my memory’s difference was 338.4326171875 and was increased to 338.947265625 after I simply printed the number 1. Beware of GMod updates increasing the amount of memory on startup, and thus, causing false positives.
The memory returned will be different on different operating systems. You can use these functions to check which operating system the client is running, and compare accordingly.
Making sure the functions you’re using are not detoured is an important thing. Detoured functions usually mean either that an addon has detoured it or the player has detoured: probably to cheat or find out how your AC functions. It’s important you don’t ban everybody just because Wiremod or whatever decided to detour RunString, so take into consideration the fact that detouring functions is a legitimate thing if you’re a legitimate developer. Here’s a few simple methods on making sure your functions are safe.
First off, save your functions. At the top of your anti cheat file (hat-trick.lua for me), save a local copy of functions you want to use. That way, you’ll (hopefully) have access to the original functions. Cheats won’t be able to simply detour your functions then. I’ll use RunString as an example.
local RunString = RunString
Another thing to note is that you may want to simply save a copy of the global table using table.Copy. I’m not sure what the ramifications of this are, but I’m sure there’s something wrong with it and someone will probably post about why it’s a bad idea, but that’s one thing you are able to do.
Second of all, pretty much all detours look like this.
local RS = RunString function RunString(...) -- New code return RS(...) end
They may specify an amount of arguments, they may not return anything, or they may place their code after the original function is called. It all varies. The point is, it looks something like that. Fortunately for us, there’s a way to detect this fairly easily using debug.getupvalue. Upvalues are basically external local variables for functions. See how the original function is external to the detour’s code? That’s an upvalue, and since the original version of RunString has no upvalues, if you check RunString and it has an upvalue, it’s detoured.
if (debug.getupvalue(RunString, 1)) then -- It's detoured end
Third of all, the classic source checking. Use debug.getinfo to check where a function came from. Really though, everything this function returns is useful, but for now, the source is what we want. Since RunString is a C function, its source should be listed as [C] or =[C], depending on whether or not you’re checking short_src or source from the return value.If it has been detoured, its source won’t be [C] unless you’re dealing with the 1% of cheaters (or have some addon detouring it). source and short_src will change depending on the identifier used to load the script. In most cases, this is simply the name of the file where the function was detoured.
Fourth of all, and probably the most interesting, is offset calculation, which only works with C++ functions. Each function has an address (or an identifying number in some cases), and these addresses change every time you load the game. The great thing is that their offsets stay the same, so one address will always be the same distance from another address. We first need a base function: a function that will serve as the base for our offset calculation. This can be almost any function. I’ve chosen to use the Msg function. Printing out RunString looks like this.
Again, this will change every time you load the game. We can use string.format to easily get the address of something like a function.
local function GetAddress(func) return string.format("%p", func) end
This returns a number for the function. First, we need to get the address for the Msg function.
local baseFunction = Msg local baseAddress local function GetAddress(func) freturn string.format("%p", func) end baseAddress = GetAddress(baseFunction)
The baseAddress variable now contains 340483752, which is the decimal form of 0x144b5ea8. Next we need to write a function to calculate the offset.
local function GetOffset(func, base) local funcAddress = GetAddress(func) -- Get the address of the passed function. local baseAddr = !base and baseAddress or GetAddress(base) -- Either use another function for the base or use the one at the top of the script. return math.abs(baseAddr - funcAddress) -- Return the distance between the two addresses. end
We can combine these to get this.
local baseFunction = Msg local baseAddress local function GetAddress(func) return string.format("%p", func) end local function GetOffset(func, base) local funcAddress = GetAddress(func) -- Get the address of the passed function. local baseAddr = !base and baseAddress or GetAddress(base) -- Either use another function for the base or use the one at the top of the script. return math.abs(baseAddr - funcAddress) -- Return the distance between the two addresses. end baseAddress = GetAddress(baseFunction)
We’re all set! Now let’s test it out. Add this to the bottom of the script.
If you’re using the Msg function for the base and RunString as the operand, hopefully you’ll see its offset is 1208. Perhaps it’s different, depending on your addons and such. Go ahead and restart the server - the number will stay the same! Something to note with this method is that some functions (like print for example) are “builtin” functions and will always return the same number. Using tostring on print will result in this.
Because of this, it is not possible to calculate offsets with print as either the base or the operand, unless both functions are builtin (since they’ll both have static numbers instead of just one of them having a static number). There are other “builtin” functions like print, so keep that in mind. As the name implies, the functions are built into Lua itself, so these are the functions that come with Lua and not GLua. Using offset calculation, you’ll be able to compare current offsets with expected offsets. If the offset is different, somethin’s fucky (or it’s those meddling RunString addons!). Changing your base function will change the base address, which changes the offset. If I change my base function to something else, 1208 will be meaningless to me because that’s the offset of RunString from Msg, not the offset of RunString from whatever other function. Different base function, different offset to whatever you feed the GetOffset function.
-- Assuming the base function is Msg... if (GetOffset(RunString) != 1208) then -- It's detoured end
Beware of GMod updates changing the offsets of functions. This will get you false positives if you’re not on top of this sort of stuff.
For the fifth and final method, hashing. string.dump can be used on a function to get its bytecode. You can then generate a hash using util.CRC. string.dump only works on functions that are made in Lua, so if it DOESN’T error while being used on a C function like RunString, we know it has been detoured.
local detoured = pcall(function() string.dump(RunString) end)
If it’s a Lua function, you can simply compare the hash of the dump with a legitimate hash of the function. Beware of GMod updates changing functions, and thus, changing the hash of the function.
Concommands and Cvars
For cheats made in Lua, you can detect these by calling concommand.GetTable and checking for concommands either related to certain cheats or just checking if they have a specific string in it (e.g, aimbot, bhop, etc). The thing about this, though, is that it only works for concommands added through Lua. It doesn’t have any concommands added through C++. I actually just recently brought an idea up in another thread. Since most cheats made in C++ add some sort of concommand or cvar and concommand.GetTable doesn’t work with C++ commands and such, you have to detect these commands in a different way. Fortunately, it’s pretty easy. Since concommand.Add fails if the command was added through C++, we can see if a command or convar exists by attempting to add it ourselves.
local function CommandExists(command, callback) if (concommand.GetTable()[command:lower()]) then callback(true) concommand.Remove(command) end concommand.Add(command, function() callback(false) concommand.Remove(command) end) RunConsoleCommand(command) timer.Simple(0, function() if (concommand.GetTable()[command:lower()]) then callback(true) concommand.Remove(command) end end) end CommandExists("kick", function(exists) print("Does the kick command actually exist?", exists) end) CommandExists("cheat_command", function(exists) if (exists) then -- Somebody's been naughty! end end)
It’s a very hacky method…but what’s an anti cheat without hacky methods?
Your anti cheat should not only communicate with the server to ban the client, it should communicate with the server so the server can verify what the client is doing. If your AC only communicates with the server to ban the client, your AC can be prevented from running and the client is free to do whatever. You don’t have to communicate every single thing the AC is doing, you only have to communicate just enough so that the client will be detected if your AC is never loaded. You also preferrably want to do detections on the server, not the client, so that people attempting to reverse engineer your AC won’t be able to see what exactly
you are checking for. They can make assumptions based on what the client is sending, but this adds a layer of obscurity.
Speaking of obscurity, obfuscation. Obfuscation mangles your code into a nightmare, and can even reduce it to a single line. Most people, upon stealing a glance at spaghetti code, will throw up their arms and say they’re done. This makes your life easier because right off the bat they’re not even going to bother trying to bypass your AC. Mission accomplished. There are automatic obfuscators out there for you to use. Find out which one is right for you. Don’t even bother doing that shit by hand. Remember to save a copy of the non-spaghetti code so you can edit your anti cheat with the original, readable code.
Combining all of the tips in this thread will let you catch 99.9% of cheaters. You should err on the side of caution when it comes to detections, unless you know for a fact that they have done something they aren’t supposed to do. Something you should consider is flagging them and notifying all admins of what they were flagged for so innocent people don’t get banned in case you fucked something up along the way.
This thread will be updated as time goes on. Feel free to ask and answer questions, and again, improve the post or tell me if I said something wrong. Thank you, and good luck.