The following document will analyze the anti-cheat system “Modern Anti Cheat”. We will go over how it works and why it’s not good. The code will be appended at the end, so that you can simply scroll down and try to analyze it for yourself too.
The Script is written in LUA, which is a game-based programming language. Garry’s Mod uses a modified version, which is also known as glua. It adds extra functions for easy game programming, and also restricts some functions so that people can’t put viruses on your PC.
LUA itself is pretty similar to Javascript. You can overwrite global functions
(like console.log = 0) and you can also load external modules via require / include. There are also 2 main things i will mention: Network Messages and Detours.
“What is a detour exactly?”
You can see that the code re-defines a lot of functions that already exist, like “math.random”. They re-define it with their own 3 “check” functions before running the function itself.
local math_random = math.random function math.random(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return math_random(...) endThis now checks your runlevel everytime you use the “math.random” function. They saved the original function and changed it globally, which is known as a “detour”. If you now try and run code from a non-existant file, or from a different source like DLL Injection, then it will detect that and send a network message to the server to ban you.
net.Receive("receive_net_name", function() net.Start("send_net_name") net.WriteString("Test123") net.SendToServer() end)This little clientside LUA codeblock sends a network message over the channel “send_net_name” if it receives a net message from the server from the net channel “receive_net_name”. You can only have one net.Receive for every channel, and these channels have to be declared on the server first before they can be used. You can however overwrite these net.Receive functions for a channel, which means if you declare another one after the first one, the first one will be deleted.
Anti-Cheat Systems, or AC for short, are programs who try to detect players who are using external programs (cheats) to give themselves an advantage over others. These can range from simple “Helpers” in the game to fully developed exploits and server crashers. They mainly ruin the fun for other players, but they can even give the players using them ingame currency and other items, ruining the economy of the game.
Garry’s Mod doesn’t have an inbuilt anti-cheat, so it’s up to the server owners to protect their servers. That’s where Modern Anti Cheat comes into play. They sell it on an “Addon” site named “gmodstore.com”, with the premise of protecting your players and server from “bad people”. The problem: MAC is not good. To analyse why, we have to first discuss the types of Anti-Cheats.
There are 2 main types of anti-cheat: Serverside AC and Clientside AC.
Clientside Anti-Cheats can detect everything, but they are insecure, because the client should never be trusted.
Serverside ACs are save from that, as players can’t easily change server code, but the server can’t detect everything by itself.
For example: An Aimbot, where a player flicks and locks onto players heads to kill them quickly, can easily be detected by the server, because you can just check how quickly and precisely someone is aiming in a game. A wallhack, where you can see other players position through walls, can’t be detected by the server, because the client is simply displaying data on screen that it already has.
Modern Anti Cheat is a Clientside AC. It tries to stop the player from running their own code, and tells the server if they try to run it. It tries to achieve this by checking the runlevel and origin of the function you are trying to run. It also “detours” (saves locally) many functions, so that they can’t be changed later by a cheat.
If you now use a function that was “detoured”, then it checks your runlevel, origin and even variable name too. It checks the variable names for bad ones that, for example, include words like “hack”, “cheat” or “aim”. Even if you don’t have any bad names, if you run this function from a different context than the game ( DLL cheat for example), then it tells the server to ban you. They don’t check everything, for obvious performance reasons, but they check the most important functions you use for cheating. (for example the “Draw the GUI/HUD” function, because most Wallhacks are being added there)
Some of you may already know this from the text above, but the main problem is the networking message. The whole Anti-Cheat consists purely of the clientside, and all the server does is ban you if he gets the corresponding message. This means: If we can just force the game to never send this ban message, then we can do whatever we want. Even if we get detected, even if the code screams “Ban this guy!”, we can simply mute the networking and we are good. This problem mainly arrives from using only one non-random network channel for only banning people. Either randomize the network channel or send something else with it instead of only using it for "detection".
This sounds easier said than done, because the game doesn’t let you overwrite local variables. At the beginning of the code they save every function locally, which means you can’t simply overwrite it. They also save their own “net.Start” locally, which means they always have access to the network channel.
The main method to overcome anticheats is by loading before them. Many check if you load before them, but “MODERN Anti Cheat” doesn’t do that. This means we can simply change the net.Start message to ignore the ban messages. An easy code example (which works) is the following:
local net_xyet = net.Start function net.Start(o,p) if o == "backup_data_transfer" then return end net_xyet(o,p) endExplanation: The first line saves the net.Start function locally, so that we can re-use it later. The second re-defines the net.Start message, and the third line checks if the network-channel-name is the “ban message” one, and if yes, then it just returns without starting a network message. If it’s not the ban network channel, then it just uses the normal net.Start method we saved earlier. We now have a re-written net.Start function which works normally except if it tries to ban us, then it doesn’t do anything.
local rccf = RunConsoleCommand function RunConsoleCommand(o,p) if o == "say" then return end rccf(o,p) endThis is nearly the same as the above example, but instead of blocking the “ban” network channel, it blocks the “say” command from being executed, which means you won’t spam the chat if you cheat.
Near the end of the code is a net.Receive function that sets recieved_mac_data true:
net_recieve("m_network_data", function() recieved_mac_data = true [...] end)If you now look for this recieved_mac_data variable you will see that this controls if the Anti-Cheat is enabled or not. That means: If you just not receive this net message named “m_network_data”, then the whole Anti-Cheat won’t work. This is because in the run_complete_checks() function it checks if the variable is true, and if not then it just won’t run the checks.
Der ganze Code ist auch schöner mit Syntax Highlighting formatiert auf Pastebin zu finden: https://pastebin.com/56xZx1Jr
--[[ File Path: addons/modern-anti-cheat/lua/autorun/client/cl_mac.lua --]] -- == LOCALIZING local m_saved_os = jit.os local convar_meta = FindMetaTable("ConVar") local cusercmd_meta = FindMetaTable("CUserCmd") local convar_get_string = convar_meta.GetString local cusercmd_set_view_angles = cusercmd_meta.SetViewAngles local net_recieve = net.Receive local net_start = net.Start local net_writebool = net.WriteBool local net_writetable = net.WriteTable local net_writestring = net.WriteString local net_sendtoserver = net.SendToServer local net_readtable = net.ReadTable local net_readbool = net.ReadBool local net_readstring = net.ReadString local create_client_convar = CreateClientConVar local concommand_gettable = concommand.GetTable local get_convar = GetConVar local debug_getinfo = debug.getinfo local debug_getupvalue = debug.getupvalue local vgui_create = vgui.Create local hook_add = hook.Add local module_require = require local run_string = RunString local timer_simple = timer.Simple local timer_create = timer.Create local timer_remove = timer.Remove local print_console = print local chat_addtext = chat.AddText local render_capture = render.Capture local render_capture_pixels = render.CapturePixels local render_read_pixel = render.ReadPixel local jit_util_funck = jit.util.funck local loop_pairs = pairs local string_find = string.find local string_lower = string.lower local string_char = string.char local table_insert = table.insert local table_copy = table.Copy local safe_pcall = pcall local math_random = math.random local math_clamp = math.Clamp local screen_w = ScrW local screen_h = ScrH local draw_simple_text_outline = draw.SimpleTextOutlined local draw_simple_text = draw.SimpleText local run_console_command = RunConsoleCommand -- == LOCALIZING -- == LOCAL DATA local m_check_tbl = {pcall, error, jit.util.funck, net.Start, net.SendToServer, net.ReadHeader, net.WriteString, util.NetworkIDToString, TypeID, render.Capture, render.CapturePixels, render.ReadPixel, debug.getinfo} local bad_cheat_strings = {"aimbot", "aimware", "hvh", "snixzz", "antiaim", "memeware", "hlscripts", "exploit city", "odium", "backdoor"} local bad_file_names = {"smeg", "bypass", "aimbot", "aimware", "hvh", "snixzz", "antiaim", "memeware", "hlscripts", "exploitcity", "gmodhack", "scripthook", "ampris", "skidsmasher", "gdaap", "swag_hack", "pasteware", "unknowncheats", "mpgh", "defqon", "idiotbox", "ravehack", "murderhack", "cathack"} local bad_function_names = {"smeg", "bypass", "aimbot", "antiaim", "hvh", "autostrafe", "fakelag", "snixzz", "ValidNetString", "addExploit", "cathack"} local bad_global_variables = {"bSendPacket", "ValidNetString", "totalExploits", "addExploit", "AutoReload", "CircleStrafe", "toomanysploits", "Sploit", "R8"} local bad_module_names = {"dickwrap", "aaa", "enginepred", "bsendpacket", "fhook", "cvar3", "cv3", "nyx", "amplify", "hi", "mega", "pa4", "pspeed", "snixzz2", "spreadthebutter", "stringtables", "svm", "swag", "external"} local bad_cvar_names = {"smeg", "wallhack", "nospread", "antiaim", "hvh", "autostrafe", "circlestrafe", "spinbot", "odium", "ragebot", "legitbot", "fakeangles", "anticac", "antiscreenshot", "fakeduck", "lagexploit", "exploits_open", "gmodhack", "cathack"} local synced_cvar_names = {"sv_allowcslua", "sv_cheats", "r_drawothermodels"} local m_check_file = true local m_check_function = true local m_check_globals = true local m_check_modules = true local m_check_cvars = true local m_check_synced_cvars = true local m_check_external = true local m_check_dhtml = true local m_check_cleaning_screen = true local m_check_detoured_functions = true local m_check_backup_kick = true local m_key = "backup_data_transfer" -- == LOCAL DATA -- == TEMPORARY DATA local m_current_file = "empty" local recieved_mac_data = false local timer_name = "" local requested_ban = false -- == TEMPORARY DATA -- == UTIL FUNCS local function unsafe_player_ban(b_reason, b_info) if (requested_ban) then return end net_start(m_key) net_writebool(true) net_writestring(b_reason) net_writestring(b_info or "No Data") net_sendtoserver() requested_ban = true end local function send_backup_message() if (not m_check_backup_kick) then return end run_console_command("say", m_key) end local function get_log_information(m_dbg_tbl) local m_info = "" if (m_dbg_tbl.short_src) then m_info = "Source: " .. m_dbg_tbl.short_src end if (m_dbg_tbl.name) then m_info = m_info .. " Function: " .. m_dbg_tbl.name end return m_info end local function generate_string(string_length) local output_str = "" for i = 1, string_length do output_str = output_str .. string_char(math_random(97, 122)) end return output_str end local function m_cur() local m_debug_info = debug_getinfo(2) return m_debug_info.short_src or "Unknown" end local function m_check(func) local s, e = safe_pcall(function() jit_util_funck(func, -1) end) if (not s) then return true end if (debug_getinfo(func).short_src and m_current_file == debug_getinfo(func).short_src) then return true end return false end local function is_string_bad(b_string, b_table) for k, v in loop_pairs(b_table) do if (not v) then continue end if (string_find(string_lower(b_string), string_lower(v))) then return true end end return false end local function get_screen_capture() render_capture_pixels() local render_pixel_r, render_pixel_g, render_pixel_b = render_read_pixel(screen_w() / 2, screen_h() / 2) return render_pixel_r + render_pixel_g + render_pixel_b end local function check_bad_concommands() if (not m_check_cvars) then return end for k, v in loop_pairs(concommand_gettable()) do if (is_string_bad(k, bad_cvar_names)) then unsafe_player_ban("Bad Console Command " .. k) end end end local function check_synced_convars() if (not m_check_synced_cvars) then return end local convar_values = {} for k, v in loop_pairs(synced_cvar_names) do table_insert(convar_values, { ["convar"] = v, ["value"] = convar_get_string(get_convar(v)) }) end net_start("m_check_synced_data") net_writetable(convar_values) net_sendtoserver() end local function check_global_variables() if (not m_check_globals) then return end for k, v in loop_pairs(bad_global_variables) do if (_G[v]) then unsafe_player_ban("Bad Function/Variable " .. v) end end end local function check_external(m_dbg_tbl) if (not m_check_external) then return end if (not m_dbg_tbl or not m_dbg_tbl.short_src) then return end if (m_dbg_tbl.short_src == "external") then unsafe_player_ban("External bypass ", get_log_information(m_dbg_tbl)) end end local function is_bad_function(m_dbg_tbl) if (not m_check_function) then return end if (not m_dbg_tbl or not m_dbg_tbl.name) then return end if is_string_bad(m_dbg_tbl.name, bad_function_names) then unsafe_player_ban("Bad function name " .. m_dbg_tbl.name, get_log_information(m_dbg_tbl)) end end local function is_bad_file_name(m_dbg_tbl) if (not m_check_file) then return end if (not m_dbg_tbl or not m_dbg_tbl.short_src) then return end if is_string_bad(m_dbg_tbl.short_src, bad_function_names) then unsafe_player_ban("Bad file name " .. m_dbg_tbl.short_src, get_log_information(m_dbg_tbl)) end end local function check_screen_cleaner() if (not m_check_cleaning_screen or m_saved_os == "OSX" or m_saved_os == "Linux") then return end if (get_screen_capture() ~= 0) then unsafe_player_ban("Screen capture returned invalid results") end end local function check_detoured_functions() if (not m_check_detoured_functions) then return end for k, v in loop_pairs(m_check_tbl) do if not m_check(v) then unsafe_player_ban("Detoured a function located at " .. debug_getinfo(v).short_src) end end end local function run_complete_checks() if (not recieved_mac_data) then return end if (requested_ban) then send_backup_message() end safe_pcall(function() check_bad_concommands() check_synced_convars() check_global_variables() check_screen_cleaner() check_detoured_functions() end) end -- == UTIL FUNCS -- == STARTUP CHECK m_current_file = m_cur() for k, v in loop_pairs(m_check_tbl) do if not m_check(v) then unsafe_player_ban("Detoured backend functions before autorun") end end -- == STARTUP CHECK -- == DETOURED FUNCTIONS function table.Copy(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return table_copy(...) end function require(args) local m_run_info = debug_getinfo(2) check_external(m_run_info) if (is_string_bad(args, bad_module_names) and m_check_modules) then unsafe_player_ban("Bad module " .. args, get_log_information(m_run_info)) end is_bad_file_name(m_run_info) is_bad_function(m_run_info) module_require(args) end function vgui.Create(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return vgui_create(...) end function hook.Add(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) hook_add(...) end function RunString(code, identifier, HandleError) local m_run_info = debug_getinfo(2) if (m_run_info.short_src and m_run_info.short_src == "lua/vgui/dhtml.lua" and m_check_dhtml) then if (is_string_bad(code, bad_function_names)) then unsafe_player_ban("Bad RunString from DHTML") end end check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return run_string(code, identifier, HandleError) end function CreateClientConVar(name, default, shouldsave, userdata, helptext) local m_run_info = debug_getinfo(2) if (is_string_bad(name, bad_cvar_names) and m_check_cvars) then unsafe_player_ban("Bad cvar " .. name, get_log_information(m_run_info)) end check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return create_client_convar(name, default, shouldsave, userdata, helptext) end function debug.getinfo(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return debug_getinfo(...) end function timer.Remove(id_str) local m_run_info = debug_getinfo(2) if (id_str == timer_name) then unsafe_player_ban("Tried to remove timer " .. id_str, get_log_information(m_run_info)) end check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return timer_remove(id_str) end function math.random(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return math_random(...) end function math.Clamp(...) local m_run_info = debug_getinfo(2) check_external(m_run_info) is_bad_file_name(m_run_info) is_bad_function(m_run_info) return math_clamp(...) end -- == DETOURED FUNCTIONS -- == NETWORK RECIEVERS net_recieve("m_validate_player", function() net_start("m_validate_player") net_sendtoserver() end) net_recieve("m_network_data", function() recieved_mac_data = true m_check_file = net_readbool() m_check_function = net_readbool() m_check_globals = net_readbool() m_check_modules = net_readbool() m_check_cvars = net_readbool() m_check_synced_cvars = net_readbool() m_check_external = net_readbool() m_check_dhtml = net_readbool() m_check_cleaning_screen = net_readbool() m_check_detoured_functions = net_readbool() m_check_backup_kick = net_readbool() m_key = net_readstring() end) -- == NETWORK RECIEVERS -- == TIMERS timer_name = generate_string(18) timer_create(timer_name, 45, 0, run_complete_checks) -- == TIMERS -- == SETUP timer_simple(0, function() net_start("m_loaded") net_sendtoserver() end) -- == SETUP