Lua/RLua Obfuscator

Hi!
I’ve been working on my own Lua obfuscator for quite a while now, and I was wondering if anyone would like to provide some feedback.

Contrary to popular obfuscators, this one isn’t a vm obfuscator.
Instead, it mutates the code in a way that aims to provide a faster execution time, for scripts that need to be as fast as possible, while maintaining an additional level of security, making it difficult for exploiters to reverse your code. In addition, with anti decompile (which is integrated into the obfuscator), it’d be more difficult for exploiters to make scripts due to the randomization of key areas in functions (such as constants, etc)

Practicality

Before we get into an actual test script, I want to describe the practicality of this obfuscator.
I have made a tool to obfuscate all local scripts within a place, with optional settings for module scripts.

Just download an .rbxlx of the place you want to obfuscate, run it through the tool and it obfuscates with the settings you want.

I am still working on extending support for Luau scripts, with types already finished, and i’m working on support for compound operators and other Luau syntax.

Current Features

Features

Control Flow
All functions and do blocks are restructured and randomized using loops

Global Encryption and Caching
example:

print("hi") = env["pawdlpawie"]("hi")

in this case, “pawdlpawie” is cached so that it doesn’t have to constantly decrypt the key.

Important rbx…
Right now I’ve done fireserver and invokeserver, as well as findfirstchild and I plan to extend this to most used roblox functions.
Essentially it takes this line:

DamageEntity:FireServer(targetHumanoid, key, damage)

and transforms it to this line

--at top of script
local f = Instance.new("RemoteEvent").FireServer
...
f(DamageEntity, targetHumanoid, key, damage)

This is important mainly in functions, where exploiters might use the FireServer constant to identify certain functions.
Example:

local function sendDamageRequest(humanoid, amount)
    DamageEntity:FireServer(humanoid, key, amount)
end

Example Scripts
The following are scripts with different features enabled/disabled to assist with testing
CFlow enabled for all of them.
Anti decompile disabled on all scripts, filesize becomes huge

Vanilla lua script

Below is a beautified version of Ironbrew 2’s bechmark script, which tests closures, gettable and settable, obviously obfuscated


local a = (getfenv or function()
	return _ENV
end)()
local b;
local c = {}
local d = setmetatable({}, {
	__index = function(self, e)
		local f = c[e]
		if f then
			return f
		else
			local g = b(e)
			local h = a[g]
			if not h then
				local i = {}local function j(k)
					for l, m in next, k do
						if type(m) == "table" and not i[l] then
							i[l] = true;
							j(m)
						elseif l == g then
							h = m
						end
					end
				end;
				j(a)
			end;
			c[e] = h;
			return h
		end
	end
})
local n = 5482972044399938;
local o = 3139;
local tonumber = tonumber;
local tostring = tostring;
local p = tostring(tonumber)
local q = p.char;
b = function(r)
	local s, t = n, 16384 + o;
	return r:gsub("%x%x", function(u)
		local v = s % 274877906944;
		local w = (s - v) / 274877906944;
		local x = w % 128;
		u = tonumber(u, 16)
		local y = (u + (w - x) / 128) * (2 * x + 1) % 256;
		s = v * t + w + u + y;
		return q(y)
	end)
end;
local function z(A, B)
	local C, u = 1, 0;
	while A > 0 and B > 0 do
		local D, E = A % 2, B % 2;
		if D ~= E then
			u = u + C
		end;
		A, B, C = (A - D) / 2, (B - E) / 2, C * 2
	end;
	if A < B then
		A = B
	end;
	while A > 0 do
		local D = A % 2;
		if D > 0 then
			u = u + C
		end;
		A, C = (A - D) / 2, C * 2
	end;
	return u
end;
local F = -#"wait, where is the wrapper?" + 154 - 14;
while not not{} do
	if F == 113 then
		if true then
			do
				local G, H, I, J, K = -#"wait, where is the wrapper?" + 64 + 35;
				while not not{} do
					if G == -#"wait, where is the wrapper?" + 219 - 50 then
						d["15704E3CBB"]('GETTABLE testing.')
						G = 13
					end;
					if G == 36 then
						d["15704E3CBB"]('CLOSURE testing.')
						G = -#"lol ib2 fork" + 226 - 26
					end;
					if G == -#"yes no vm here" + 222 + 26 then
						for L = 1, H do
							K[d["098B744A2FEE1F95"](L)] = 'EPIC GAMER ' .. d["098B744A2FEE1F95"](L)
						end;
						G = -#"imagine imaging???" + 34 + 13
					end;
					if G == 120 then
						d["15704E3CBB"]('Iterations: ' .. d["098B744A2FEE1F95"](H))
						G = 36
					end;
					if G == 72 then
						d["15704E3CBB"]('IronBrew 2:tm: Benchmark-y Meme')
						G = -#"okay guys now table.concat = print" + 37 + 35
					end;
					if G == -#"imagine vm obfuscating :flushed:" + 61 + 22 then
						d["15704E3CBB"]('Time:', d["BCA698CD92"]() - I .. 's')
						G = -#"obfuscated using moonsecv3" + 252 + 14
					end;
					if G == -#"yes buy luraph" + 65 + 19 then
						for L = 1, H do
							K[1] = K[d["098B744A2FEE1F95"](L)]
						end;
						G = -#"lol ib2 fork" + 95 - 32
					end;
					if G == -#"hmmmmmm" + 70 - 25 then
						H = 100000;
						G = 120
					end;
					if G == -#"imagine vm obfuscating :flushed:" + 15 + 27 then
						for L = 1, H do
							(function()
								local M = -#"i hate this" + 243 + 13;
								while not not{} do
									if M == 245 then
										if not true then
											d["15704E3CBB"]'Hey gamer.'
										end;
										break
									end
								end
							end)()
						end;
						G = 101
					end;
					if G == 188 then
						I = d["BCA698CD92"]()
						G = 43
					end;
					if G == 240 then
						d["15704E3CBB"]('Total Time:', d["BCA698CD92"]() - J .. 's')
						break
					end;
					if G == -#"i hate this" + 238 + 20 then
						K = {}
						G = 234
					end;
					if G == -#"lol ib2 fork" + 33 + 35 then
						I = d["BCA698CD92"]()
						G = -#"wait, where is the wrapper?" + 326 - 52
					end;
					if G == 43 then
						J = I;
						G = 10
					end;
					if G == 13 then
						I = d["BCA698CD92"]()
						G = 70
					end;
					if G == -#"loadstring = print" + 275 - 51 then
						d["15704E3CBB"]('SETTABLE testing.')
						G = -#"hmmmmmm" + 32 + 31
					end;
					if G == -#"" + 113 - 12 then
						d["15704E3CBB"]('Time:', d["BCA698CD92"]() - I .. 's')
						G = 206
					end;
					if G == -#"" + -15 + 44 then
						d["15704E3CBB"]('Time:', d["BCA698CD92"]() - I .. 's')
						G = 142
					end
				end
			end
		end;
		break
	end
end

keep in mind, this script isin’t using my custom loader. which is a loadstring meme with some fun integrity checks. (Decided to not include that and soley focus on roblox related obfuscation)

Platform lock: ROBLOX

same script, different settings.


local a = (getfenv or function()
	return _ENV
end)()
local b;
local c = {}
local d = setmetatable({}, {
	__index = function(self, e)
		local f = c[e]
		if f then
			return f
		else
			local g = b(e)
			local h = a[g]
			if not h then
				local i = {}local function j(k)
					for l, m in next, k do
						if type(m) == "table" and not i[l] then
							i[l] = true;
							j(m)
						elseif l == g then
							h = m
						end
					end
				end;
				j(a)
			end;
			c[e] = h;
			return h
		end
	end
})
local n = 2915560711885084;
local o = 8418;
local tonumber = tonumber;
local tostring = tostring;
local p = tostring(tonumber)
local q = p.char;
b = function(r)
	local s, t = n, 16384 + o;
	return r:gsub("%x%x", function(u)
		local v = s % 274877906944;
		local w = (s - v) / 274877906944;
		local x = w % 128;
		u = tonumber(u, 16)
		local y = (u + (w - x) / 128) * (2 * x + 1) % 256;
		s = v * t + w + u + y;
		return q(y)
	end)
end;
local z, A, B = Instance.new("RemoteEvent").FireServer, Instance.new("RemoteFunction").InvokeServer, Instance.new("Part").FindFirstChild;
local function C(D, E)
	local F, u = 1, 0;
	while D > 0 and E > 0 do
		local G, H = D % 2, E % 2;
		if G ~= H then
			u+=F
		end;
		D, E, F = (D - G) / 2, (E - H) / 2, F * 2
	end;
	if D < E then
		D = E
	end;
	while D > 0 do
		local G = D % 2;
		if G > 0 then
			u+=F
		end;
		D, F = (D - G) / 2, F * 2
	end;
	return u
end;
local I = -#"ew?" + 178 + 45;
while not not{} do
	if I == -#"i hate this" + 202 + 29 then
		if true then
			do
				local J, K, L, M, N = 142;
				while not not{} do
					if J == -#"lol ib2 fork" + 84 - 55 then
						L = d["69F1"].clock()
						J = 135
					end;
					if J == 62 then
						for O = 1, K do
							N[1] = N[d["B205300741C4A1AB"](O)]
						end;
						J = -#"" + 15 + 23
					end;
					if J == 101 then
						for O = 1, K do
							N[d["B205300741C4A1AB"](O)] = 'EPIC GAMER ' .. d["B205300741C4A1AB"](O)
						end;
						J = 195
					end;
					if J == -#"yes buy luraph" + 248 - 18 then
						for O = 1, K do
							(function()
								local P = -#"bro looking at my code >:(" + 124 + 44;
								while not not{} do
									if P == -#"emoji bytecode soon" + 127 + 34 then
										if not true then
											d["DEB6DA7D4D"]'Hey gamer.'
										end;
										break
									end
								end
							end)()
						end;
						J = -#"imagine vm obfuscating :flushed:" + 142 - 42
					end;
					if J == -#"loadstring = print" + 249 - 25 then
						d["DEB6DA7D4D"]('Iterations: ' .. d["B205300741C4A1AB"](K))
						J = -#"i hate this" + 211 - 50
					end;
					if J == 72 then
						d["DEB6DA7D4D"]('SETTABLE testing.')
						J = 245
					end;
					if J == 135 then
						M = L;
						J = 216
					end;
					if J == 221 then
						N = {}
						J = 101
					end;
					if J == -#"bro looking at my code >:(" + 241 + 22 then
						L = d["69F1"].clock()
						J = -#"imagine imaging???" + 119 - 39
					end;
					if J == -#"loadstring = print" + 115 + 45 then
						d["DEB6DA7D4D"]('IronBrew 2:tm: Benchmark-y Meme')
						J = -#"ew?" + -9 + 19
					end;
					if J == -#"imagine imaging???" + 268 - 55 then
						d["DEB6DA7D4D"]('Time:', d["69F1"].clock() - L .. 's')
						J = 107
					end;
					if J == 150 then
						d["DEB6DA7D4D"]('CLOSURE testing.')
						J = -#"yes no vm here" + 42 - 11
					end;
					if J == 107 then
						d["DEB6DA7D4D"]('GETTABLE testing.')
						J = 237
					end;
					if J == 7 then
						K = 100000;
						J = 206
					end;
					if J == -#"imagine imaging???" + 107 - 11 then
						d["DEB6DA7D4D"]('Total Time:', d["69F1"].clock() - M .. 's')
						break
					end;
					if J == -#"hmmmmmm" + 306 - 54 then
						L = d["69F1"].clock()
						J = -#"wait, where is the wrapper?" + 298 - 50
					end;
					if J == -#"imagine vm obfuscating :flushed:" + 129 - 29 then
						d["DEB6DA7D4D"]('Time:', d["69F1"].clock() - L .. 's')
						J = 72
					end;
					if J == 38 then
						d["DEB6DA7D4D"]('Time:', d["69F1"].clock() - L .. 's')
						J = -#"obfuscated using moonsecv3" + 72 + 32
					end
				end
			end
		end;
		break
	end
end

should work on roblox studio. Currently platform lock consists of changing binary operations to comply with roblox syntax, such as a = a +1 to a +=1, helps prevent people from running this stuff on replit or vscode with vanilla lua5.1. I plan to add more ofc because that much isin’t secure enough.
I need to add integrity checks after I finish adding full Luau support

One thing to note: global encryption might mess up if you set global variables later on. havent tested that yet but my official reply as rn: “oops?”**
Anyways, thanks for reading! I’ve always been invested in obfuscation and security so this was a really fun project I look forward to continuing.

Obfuscation should not be relied on as your main form of security. Your game is only as secure as you make it, no amount of obfuscation will stop an exploiter when you don’t properly secure interactions in your game.
Prioritize secure client-server interactions over safeguarding your code, since exploiters may not even need to analyze your code to make a script that gives them infinite money, kick/ban capabilities, etcetera.
Obfuscation is, essentially something to slow down exploiters and reverse engineering, what’s most important is your server code, and making it secure so that exploiters can’t ruin the experience for others.

5 Likes

Yikes, getfenv and equating obscurity to security. Two things I don’t stand by. I get it’s especially difficult or even impossible to create obfuscators without getfenv but in 2021 I wouldn’t encourage any developer to be using it since it implies that you’re doing something with globals.

Appreciate that you’ve taken the time to highlight that obfuscation isn’t a catch-all to security problems with an experience’s code and that additional steps need to be taken on a case-by-case basis but that needs to be super clear. There’s a large swath of Roblox developers who have a dangerous obsession for implementing obscurity in their code for little to no additional benefit to their experiences.

It’s not exactly direct feedback but just wanted to add that comment. Good job though, if it’s served as a learning experience for you and a way to test your programming ability!

7 Likes

I didn’t know there was an entire resource dedicated to Luau preformance so I’ll take a look at that and modify as needed

I do spend a lot of type writing sanity checks on the server for all client requests through remotes, etc.
Like I have previously said, it’s definitely important to secure your server over throwing your localscripts through an obfuscator.

Like the wikipedia article states,

Security by obscurity alone is discouraged and not recommended by standards bodies. The National Institute of Standards and Technology (NIST) in the United States sometimes recommends against this practice: “System security should not depend on the secrecy of the implementation or its components.”[7]

The obfuscator aims to deter exploiters, and randomize things on each update, making it harder for scripts to properly function after each update.
Then again, I only briefly said how it’s supposed to be an additional layer of security, instead of the only thing protecting your game. I have modified the thread to highlight that more explicitly

Like Kerckhoffs said,

The principle holds that a cryptosystem should be secure, even if everything about the system, except the key, is public knowledge

Although this was for cryptography, I believe this holds true at least to some extent with game design, especially with requests to the server. You can minimize the things an exploiter can do as long as you put the effort into secure interactions.

Thank you for the feedback, and the resources and I look forward to improving upon my current obfuscator.

This was just a hobby since I’ve always been fascinated by obfuscation, something to work on when I don’t wanna load studio and program an actual game and I definitely still have a lot to learn about obfuscation, Luau and lua in general. Thanks again