(august 2018 revision: updated this post to be less… annoying, also moved to learning resources)
intro
script injectors are programs that allow exploiters to run their own scripts inside of roblox’s lua environment. this is bad for a lot of reasons because exploiters can:
- steal pretty much any asset that appears on the client
- abuse devs’ uninformed network coding practices to exploit FE games
- acess and edit the source of any script or module on the client
- download the entire clientside datamodel as an rbxl
as roblox grows, exploting games is going to become more of a problem. not only will the amount of people using exploits increase, so will the amount of competent people working to make new exploits. since the problem stems from lua itself as a programming language, and its unreasonable to expect roblox to completely restructure the entire security system from the ground up, it’s up to us developers to use more responsible coding practices to keep our games safe.
protecting your assets
because these id7 exploits have become so accessible, from now onward, you should make all of the following assumptions:
any asset that appears in a game published on the site is no longer safe
many of these injectors include some sort of function that allows exploiters to save instances to the disk, any asset in the clientside datamodel can be stolen. personally, i dont think leaked assets are that big of a problem because most of us have the integrity to not use assets stolen from other devs’ games, but others may find it much more upsetting. to lower of the chances of massive asset leaks, store all of your models or maps in the server storage rather than in replicated storage. for models that need to be accessed by the client, clone all of the assets in the replicated storage, delete the originals, and hold references to the cloned models in the script. accessing the cloned models in the scripts memory will be out of the question, as the small amount of competent v3rmies actually capable of doing so probably dont care enough to try. anyone trying to steal your assets wont have any choice but to wait until they actually appear in the workspace
any code in a game published on the site is no longer safe
the same methodology for stealing assets applies to stealing scripts. to keep your code safer ( that is, a lot more inaccessible, but not completely safe), you can place all of your main code in modules. after you require the modules, delete them; the code will continue to run as long as you dont delete the script used to require the modules. this should keep your code more protected, but the lua bytecode can still be decompiled, albeit in a much less readable format. to take this a step further, you can use an obfuscation program to make your code even harder to read (but still not absolutely safe).
while we can make our games less vulnerable with better network code, there’s nothing we can do at the moment about stolen assets. in the future, i think roblox should penalize devs who are caught with stolen assets, but that involves a whole different slew of politics that i dont want to get into right now. at the moment, asset stealing really isnt a problem. if it becomes a problem in the future, we can hope roblox will step in with new policies to prevent it from happening.
method hooking and other ways script injectors are used to exploit FE games
preface: a brief intro to metamethods and metatables
metamethods are special functions that are invoked when specific operations are preformed on a given table. for these functions to be invoked and treated as a metamethod, they must be assigned to a specific index inside of a metatable. that metatable is then assigned to a table with the setmetatable()
lua function.
the __index metamethod
the one particular metamethod we care about in this case is the __index
metamethod, which is called when a value is indexed from a table that doesnt exist. if the table’s index metamethod returns a value, rather than returning nil, the index operation will return the value that was returned from the function:
local mt = {
__index = function(self, index)
print("__index invoked")
return index.." made money off of the iraq war"
end
}
local our_boy_table = {
["george bush"] = "an alright boy"
["al gore"] = "certified goodboy"
}
setmetatable(our_boy_table, mt)
print(our_boy_table["dick cheney"])
> __index invoked
> dick cheney made money off of the iraq war
(this is a simplification for the sake of readability)
the instance metatable
every instance in roblox shares the same metatable. (why? idk. ask urist or something). from my understanding, this metatable has a generic __index
metamethod for returning the properties, events, and methods of instances:
metatable detouring (method hooking)
under normal circumstances, the instance metatable is completely inaccessible with the normal script security identity level. unfortunately for us, the .dll CE injection “scythe localscript exploit [PATCHED]” videos circa 2012 have now been replaced by annoying 15 year olds with bad hair that talk a little bit too loud showing off their latest and greatest very terrible murderprogram they just bought from a v3rmillion back alley using the dominus they scammed off of some other just as obnoxious preteen. because the metamethods of the instance metatable can be overwritten, exploiters can replaced the __index
metamethod with their own function, which is called whenever any member, is indexed from any instance, in any script throughout the entire client.
the following code block is a hypothetical exploit written for the hypothetical game Gunshooter: Online. the script causes the exploiters weapon to automatically kill the closest enemy to him, as well as negate all incoming damage. gunshooter online only uses a single remote to handle all of the network code, and distincts between different events by passing a string through the first argument of the remote. the hypothetical exploiter figured this out by placing listeners on the remote events in the game with method hooking prior to writing this exploit. he could have also read the client code to better understand what the specific arguments of the remote events were doing
local instance_meta = globalgetmetatable(game) -- the exploiter uses the function in their script injector's enviornment to get the instance metatable
local old_index = instance_meta.__index
local remote_event = game.ReplicatedStorage.RemoteEvent
--- godmode ---
local spoof_event = Instance.new("BindableEvent")
remote_event.OnClientEvent:connect(function(...)
local event = (...)[1]
if event ~= "TakeDamageClient" then -- prevent the take damage from being fired
spoof_event:Fire(...)
end
end)
instance_meta.__index = function(instance, index)
--- instakill ---
if index == "FireServer" then
return function(...) -- replaces FireServer in the script it was called from
local event = (...)[1] -- first argument
if event == "NewBullet" then
local nearest_head_pos = ConvenientFindNearestEnemyHeadPositionFunction()
local bullet_origin = nearest_head_pos + Vector3.new(0,1,0);
local bullet_direction = Vector3.new(0,-1,0)
local bullet_speed = 10000
local bullet_damage = 10000
local FireServer = old_index(instance, "FireServer")
FireServer(event, bullet_origin, bullet_direction, bullet_speed, bullet_damage)
end
end
elseif index == "OnClientEvent" then
return spoof_event.Event -- reroute incoming client events to our bindable spoof_event for godmode
else
return old_index(instance, index)
end
end
the insta-murder part of this code can be rewritten to run in only 13 lines (i wasted a lot of space to make sure the code was more readable). a year ago i thought using FE was the end-all solution to exploiters, im not sure how many others still have similar biases, but i can say for sure that FE is definitely not that. in the next section im going to try to help establish some network coding best practices that will keep your games safe and away from ending up in a situation similar to gunshooter online.
method hooking quick fixes
you need to know: the solution is not keeping exploiters from method hooking. the client can not be trusted, and any solution that is implemented fully on the client can, and will eventually be bypassed. despite this fact, there are a few things we can do to make it more difficult for exploiters:
- using syntax like this will allow you to call methods without invoking any hijacked __index metamethods. however, if the method is already hooked prior to setting the function to a variable, (for example, if you reset your local scripts), this method won’t do anything.
local FireServer = Instance.new("RemoteEvent").FireServer
-- and whenever a remote needs to be fired
FireServer(AnyRemoteEvent, Arg1, Arg2, Arg3, ...)
- quick checks for detoured functions
local FireServer = RemoteEvent.FireServer -- no detour, original function
-- time passes
if FireServer ~= RemoteEvent.FireServer then
--personally i dont reccommend actually doing anything here noticable for reasons
--explained later
end
--=================--
-- some detour functions, like the example i used, return a new instance of a
-- function every time they're called, which allows us to do this
if RemoteEvent.FireServer ~= RemoteEvent.FireServer then
end
-- this is super easy to bypass though, because all the exploiters would have to do
-- is not pass a new anonymous function every time
good networking practices: authoritative server
the server authority model is where the server has the ultimate jurisdiction over the current game and client states. when a player wants to preform any kind of action in the game that would influence the game’s state, the player’s state, or the states of any other players, the server has to first verify if that action is valid before making any changes to any other players. in a survival game example, if a player wants to drop an item from their inventory onto the ground, the server first has to make sure that item exists in the players inventory, and then updates states accordingly to reflect that change.
server and client states: parallel, but not the same
there are right ways to do server authority and there are wrong ways. for the sake of simplicity, i’m going to keep using my example of the survival game inventory.
for foolproof reasons, let me cover the most wrong way to handle this. the client relies on its own logic to decide when items are added to the inventory. the server doesnt keep track of the client’s inventory state, and when a player drops an item this interaction occurs (c and s are client and server, respectively)
c: i want to drop a stick, here's the data of all of the items in my inventory s: you have a stick in your inventory. you can drop the stick s: there is a stick here now
this is bad for obvious reasons because the client can either change the data of his inventory or spoof the information being sent to the server and following situation could occur
c: i want to drop 1000 gold, here's the data of all of the items in my inventory s: you have a 1000 gold in your inventory. you can drop the gold. s: there is 1000 gold here now
the first step in ensuring that this doesnt happen is to keep track of the players inventory on the server
c: i want to pick up this stick on the ground s: the stick exists on the ground in this game state, and you are close enough to the stick to pick it up, and you still have enough inventory space to pick it up. you can pick up the stick s: <add a stick to player's inventory> s: for all players to whom the stick may concern: the stick is gone
while this is a step in the right direction, it’s still a flawed approach. we want our game to be responsive and safe. if someone is playing with 250 ping we dont want them to have to wait a split second every time they try to pick up an item. we need to allow the client to have a state that is parallel to, not reliant on, the server. heres how we do this
c: i picked up this stick c: <adds a stick to its own inventory> s: all of the conditions for you to pick up the stick have been met, you can keep the stick. s: <add a stick to player's server inventory> s: sorry to tell you this boys-- we're all outta sticks!!
client was still able to pick the stick up in their game state, but on the server the conditions aren’t met (the client is too far away, another player picked it up first, etc) we don’t add that stick to the server version of the client’s inventory, and we send an update to the client that their stick needs to be removed from their inventory. if an exploiter bypasses the update so they can keep the stick, they won’t be able to do anything with it. this is because we have our authoritative version of the client’s inventory on the server, and any actions the exploiter tries to preform with the stick will be invalid. this method of allowing the client to lean on its own logic and correcting it when it makes mistakes, as opposed to being fully reliant on responses from the server, allows us to keep the apparent effects of latency to a minimum while not compromising any safety. while this is all well and good for a simple survival game, more difficult problems arise when we increase the complexity of the systems in our game.