Script sandboxing with behaviour detection?

  • What are you attempting to achieve? (Keep it simple and clear).

I’m trying to sandbox scripts and detect the behaviour of the sandboxed script. I don’t want the script to be able to change anything just make it think it did.

  • What is the issue? (Keep it simple and clear - Include screenshots/videos/GIFs if possible).

I looked at setfenv (Wiki) and it seems like that would be a possible solution to my problem but the documentation is really lacking so I have no clue how to use it.

  • What solutions have you tried so far? (Have you searched for solutions through the Roblox Wiki yet?)

I have googled

What exactly do you need this for? Analyzing an obfuscated script or something like that? If so, you will need to create a full sandbox and make it log/print all actions it does, such as indexing global env variables, invoking __index, __namecall, etc.

If the only thing you’ve tried so far is “googled” then you will probably have a hard time making a full sandbox just on your own, so I recommend you to read this sandboxing article on the roblox wiki. It provides a good example code and explains some stuff.

Here is a list of things you should learn if you want to make a sandbox:

  • Metatables & metamethods
  • What is userdata and newproxy(true)
  • What are weak tables (for cache)
  • Environments (setfenv, getfenv) and how they work

getfenv and setfenv are functions that allow the manipulation of environment of functions in Lua. The environments themselves are tables that contain all the variables of the function (check this for more information).

getfenv receives a single argument, either the function with the environment you want to obtain for whatever reason, or an integer that points to the scope. In case it’s a 0, the returned table will be the global environment of the function it was called in. Else, you can specify a level from 1 (the current environment) to whatever depth you want (if you go too far, the returned environment will be the global). So, 2 will return the environment of the function that called the current function and so on.
Once you get the environment table, you can edit all variables within just like you would set the values of a normal table.

setfenv receives 2 arguments, the function you want to edit (or 0 for global) and the environment table. You can create a new environment by creating a new table and setting the variables in it, though you should note that you also have to import all built-in functions as they also count as variables (you can replace the math table with something you want, it’s just not recommended). By doing this, you can essentially block the access of a function to global variables such as game and Instance, essentially making it unable to manipulate the DataModel.

A few examples of what I just said:

--We are currently in the global environment, meaning that the current environment will be equal to it
print(getfenv(0)==getfenv(1))--true

--As said above, you can easily change a variable of an environment with getfenv
local env = getfenv(0)
env.test = "This is a global"
print(test) 

--Now we are going to sandbox a function so that it can only print

local function func()
	print(globalvar) --will print the globalvar value in the environment
	game.Workspace.Baseplate:Destroy() --this will error
end
local newenv = {
	globalvar="this is a global variable only available to this function",
	print = print --we need to import print into this new environment, all other globals don't exist
}
setfenv(func,newenv)
func()

Thanks to @Amiaa16 for pointing out my mistakes.

This is incorrect. setfenv's 2nd argument has to be a table. Its first argument can indeed be 0 if you want to edit the current global env, but the second one always has to be a table.

In other words, this works:

setfenv(func, getfenv(0))

and this does not:

setfenv(func, 0) --bad argument to #2, table expected, got number

getfenv(1) returns the env of the current function (the one that called getfenv). It doesn’t return the env of the function that called the current function, that’s what putting 2 does.


Btw in my opinion you should give at least a brief explaination of what an env is and how it works before explaing setfenv/getfenv, since it might be confusing for people that are new to it - “why does setfenv change the variables of a function?”, “what does {print = print} do in it? Is it some env identifier or what?” (personal experience)

1 Like

I’ve written a Lua sandbox implementation that I have not been able to break out of and those I’ve asked to test it have not been able to break out of either. When I suggested using it at one point in the Roblox code base to a member of the Lua Core team they were kind enough to test it out and replied with something along the lines of:

To use it grab a copy of the source code from github and put it into a module. You can begin running the sandbox like so:

require(workspace.sandbox)()
... rest of the script

To log all table access, simply change line 17 from

function Capsule:__index(k)
    return self[k]
end

to

function Capsule:__index(k)
    print('accessed table', self, 'with key', k)
    return self[k]
end

If you want to log all function calls, you can change line 89 to

wrappedFunc = function(...)
    print('Call into sandbox with arguments', ...)
    return unwrap(func(wrap(...)))
end

and on line 134

wrapped = function(...)
    print('Call out of sandbox with arguments', unwrap(...))
    return wrap(func(unwrap(...)))
end

Using the two above changes the function name will usually be printed first (if not local or accessed from a table), then the names of the non-local arguments, then the call will print the values of the arguments

Important Notes

  • In order to be sandboxed appropriately, the module must be required at the top of the file to be sandboxed. Failure to do so may result in unwrapped local variables since they are not stored in the environment. This may result in strange bugs when mixing wrapped and unwrapped code.
  • This library is to be used for debugging only. Not enough testing has been done to confirm that this code is safe for security-critical production environments. (In fact, I’m thinking of a way to escape it right now. I’ll check on it.)

Edit: I was able to break out of the sandbox by passing a callback out of the sandbox and receiving unsandboxed variables which I then assigned to upvalues. I’ve submitted an update on GitHub and edited this post’s line numbers and instructions to match the new version. As I said, this is for debugging only and is not yet production ready.

5 Likes