Recently, I engaged in a discussion within my community about de-obfuscating Lua code. This was requested after many were puzzled about how "random-looking" characters could possibly result in non-faulty code being executed. In this blog, we will examine a short code sample which will get the cogs in your brain working to potentially think about de-obfuscating larger scripts. Although this blog post uses Lua as an example, it is still very relevant to other scripting/programming languages as the same techniques of obfuscation are usually used.
So firstly, let's get the most basic thing out of the way:
In pure English, obfuscation is the action of making something obscure, unclear, or unintelligible. In the same way, when applied to code, obfuscation is used to prevent source code from being easily read or understood.
It is a common misconception that obfuscation completely hides source code from other people. In reality, obfuscation simply makes the code harder to read and understand, but with enough effort, it often can be de-obfuscated (as demonstrated by this blog post!).
local _ = ('t'..'a'..'s'..'k')['w'..'a'..'i'..'t']
local __ = (getfenv()[string.char(103,97,109,101)].LinkingService)
local ___ = (string[('r'..'e'..'v'..'e'..'r'..'s'..'e')])
while (0 == 0) do
_((5^2)^-2)
__:openUrl("\104\116\116\112\115\58\47\47"..___("\108\111\108\46\121\104\99\105\114"))
end
At first glance, the code above may seem perplexing due to the string concatenations and delimiter inconsistencies, ASCII character codes, and seemingly random and confusing mathematical expressions. However, by breaking it down line by line, we can uncover its hidden functionality.
local _ = ('t'..'a'..'s'..'k')['w'..'a'..'i'..'t']
To any experienced Lua programmer, especially one familiar with Roblox Lua, it’s clear that this line accesses the task.wait
function - a function specific to Roblox.
Here's how it works: the expression ('t'..'a'..'s'..'k')
concatenates individual characters to evaluate to "task"
, while ['w'..'a'..'i'..'t']
produces "wait"
.
These strings are then combined to access task.wait
.
In Lua, writing ("task")["wait"]
is functionally equivalent to task.wait
.
Lua treats the first form as a lookup, where:
• ("task")
is used as the key to access the Roblox global variable task
• ["wait"]
specifies the wait
method within task
.
TL;DR:
local _ = ('t'..'a'..'s'..'k')['w'..'a'..'i'..'t']
becomes:
local _ = task.wait
local __ = (getfenv()[string.char(103,97,109,101)].LinkingService)
In Lua, getfenv()
retrieves the current environment of a function, which includes access to global variables.
In Roblox, this environment includes Roblox-specific globals like game
, Vector3
, TweenInfo
, and others.
string.char(103,97,109,101)
This part converts ASCII values into characters to form the word "game"
.
string.char
is a standard Lua function that converts each numeric argument from ASCII to a character, concatenating them into a single string.
Each number here represents an ASCII character:
• 103
= "g"
• 97
= "a"
• 109
= "m"
• 101
= "e"
Thus, string.char(103,97,109,101)
returns "game"
.
By substituting our findings, the expression getfenv()[string.char(103,97,109,101)]
simplifies to getfenv()["game"]
, which is equivalent to just game
in this context.
Since the square brackets [ ]
in Lua denote access to an element within a structure, and getfenv()
essentially returns a table containing global variables (like game
), we can simplify this expression by replacing getfenv()["game"]
with just game
.
This makes the line easier to read as it becomes game.LinkingService
.
So, the original line:
local __ = (getfenv()[string.char(103,97,109,101)].LinkingService)
can be simplified to:
local __ = game.LinkingService
This de-obfuscation shows how ASCII values and environment lookups can be used to obscure code, making it less readable but functionally equivalent.
local ___ = (string[('r'..'e'..'v'..'e'..'r'..'s'..'e')])
In this line, we're working with the string library, specifically aiming to access the string.reverse
function.
This is achieved by concatenating individual characters to spell out the methods name.
('r'..'e'..'v'..'e'..'r'..'s'..'e')
The ..
operarator in Lua concatenates strings, so ('r'..'e'..'v'..'e'..'r'..'s'..'e')
evaluates to "reverse"
.
By placing string["reverse"]
in square brackets, we’re accessing the reverse
function within the string
library. Using string["reverse"]
is equivalent to calling string.reverse
, but it adds an extra layer of obscurity.
Substituting our findings, the original line:
local ___ = (string[('r'..'e'..'v'..'e'..'r'..'s'..'e')])
simplifies to:
local ___ = string.reverse
local _ = task.wait
local __ = game.LinkingService
local ___ = string.reverse
If you have understood everything so far, give yourself a pat on the back - you have really got the hang of this! I'm genuinely glad that you have stuck with it; following along through detailed explanations of code de-obfuscation isn't always thrilling.
But look at where we are now! With the main variables and their values (functions) mapped out, we can recognise the purpose of each function or object in this code snippet. From hereon, we will dive into how these variables are used in the script.
while (0 == 0) do
_((5^2)^-2)
__:openUrl("\104\116\116\112\115\58\47\47"..___("\108\111\108\46\121\104\99\105\114"))
end
Right away, we can start de-obfuscating lines 4 and 5, as they are quite straightforward. Let's go line-by-line.
while (0 == 0) do
This condition here, (0 == 0)
, evaluates to true
.
Since 0
is always equal to 0
, this expression is permanently true
.
In Lua, any condition that always evaluates to true
creates an infinite loop, as we can see here.
Therefore, we can simplify this to:
while true do
This infinite loop structure will keep running as long as the program executes, repeatedly running the code within it.
_((5^2)^-2)
Here, we are passing a mathematical expression, (5^2)^-2
, as an argument to _
.
Since we know that _
from Line 1 is task.wait
, we are essentially calling task.wait()
here.
Let's break down the math before substituting it in:
• In Lua, the ^
symbol represents the "to the power of operation".
• First, 5^2
calculates to 25
(since 5 to the power of 2 is the same as 5 * 5
).
• Now we need to calculate 25^-2
, which requires us to first find 25^-1
and then square that result.
• To find 25^-1
, we take the reciprocal of 25
.
A "reciprocal" is 1 over x, where x is the number in question.
Therefore, 25^-1
is 1/25
which is 0.04 in decimal. If you're very confused about reciprocals, read about them on Wikipedia.
• But remember, we are finding 25^-2
, so we have to square the answer to 25^-1
.0.04^2
is equal to 0.0016
.
So, our simplified math result is 0.0016
.
Knowing that _
is a function (indicated by the parentheses, ()
), we are calling task.wait()
with 0.0016
as the argument.
Substituting this into our code gives us the following:
while true do
task.wait(0.0016)
__:openUrl("\104\116\116\112\115\58\47\47"..___("\108\111\108\46\121\104\99\105\114"))
end
This is already looking much more readable, thanks to our mental "de-obfuscator!"
__:openUrl("\104\116\116\112\115\58\47\47"..___("\108\111\108\46\121\104\99\105\114"))
In this line, we meet ASCII encoding again. This time though, it is embedded within two strings using escape sequences. Each ASCII code follows a backslash, and this pattern makes it easy to convert the encoded values back into readable characters.
To tackle this section, we have two convenient methods for converting ASCII-encoded sequences into readable strings. First, let's use my string conversion tool, which provides an efficient and user-friendly way to decode these sequences. This tool shines here, as it allows us to instantly replace escape sequences with their corresponding characters without additional setup, making it faster and more accessible than alternative methods.
Using my tool, we can see in the screenshots that the two strings decode to "https://"
and "lol.yhcir"
.
Alternatively, if you prefer a hands-on approach or want to test this in a local Lua runtime, you can use a Lua REPL (Read-Eval-Print-Loop).
While both pure Lua and Luau can be used, Luvit is particularly convenient due to its pretty-print feature, which displays strings in a clear and readable format.
To demonstrate, I've opened up a terminal and spun up a Luvit REPL.
Here's an example of how you can input ASCII-encoded strings and have Luvit display the decoded output:
Ultimately, both my tool and Luvit yielded the same results, but using my string conversion tool was much faster and more straightforward without additional commands or setup to get "https://"
and "lol.yhcir"
.
Continuing from where we left off, we know from our earlier de-obfuscation of ___
that it actually refers to string.reverse
.
So, in this code, the function ___
is simply reversing "lol.yhcir"
back to "richy.lol"
.
My tool makes this straightforward, with a convenient reverse button that transforms "lol.yhcir"
into "richy.lol"
with a single click:
With that done, we can concatenate the two parts, "https://"
and "richy.lol"
, forming the complete URL: "https://richy.lol"
.
Substituting back into our line of code, we get:
__:openUrl("https://richy.lol")
And remember, from Line #2, we previously discovered that __
is a reference to game.LinkingService
.
So, by substituting this as well, we reach the final, fully de-obfuscated line:
game.LinkingService:openUrl("https://richy.lol")
This line instructs the Roblox client to open my website in an external browser using a hidden service which is usually only visible to Roblox Core Scripts and exploits/executors. This shows how obfuscated code can obscure even the simplest of operations!
Congratulations on making it through this laborious process!
While we tackled a relatively simple script, the journey of de-obfuscating should get your brain working constructively, encouraging you to think critically and dig deeper.
If you're up for a challenge, see if you can de-obfuscate something even more complex.
How about diving into scripts that go beyond ASCII, like UTF encoding?
If that sounds intriguing, feel free to reach out on my socials (listed on this site).
I'm always happy to see people expanding their knowledge!
As a side note and a shortcut, we could have used my string converter from the very start to simplify the ASCII conversions.
My tool would have quickly translated the ASCII escape sequences into their characters and is designed to streamline de-obfuscation.
More features are coming soon, making the process even easier - keep your eyes peeled!
In summary, we successfully de-obfuscated the following obfuscated code:
local _ = ('t'..'a'..'s'..'k')['w'..'a'..'i'..'t']
local __ = (getfenv()[string.char(103,97,109,101)].LinkingService)
local ___ = (string[('r'..'e'..'v'..'e'..'r'..'s'..'e')])
while (0 == 0) do
_((5^2)^-2)
__:openUrl("\104\116\116\112\115\58\47\47"..___("\108\111\108\46\121\104\99\105\114"))
end
into this more readable version:
while true do
task.wait(0.0016)
game.LinkingService:openUrl("https://richy.lol")
end
This process proves that obfuscation, while it makes code harder to understand, does not truly protect source code.
In this blog, you learned:
• The basics of Lua obfuscation
• How to use ASCII and escape sequences
• Environment lookups and how getfenv()
functions in Lua
• How concatenation and string manipulation can disguise code functionality
I hope you enjoyed this walkthrough!
This blog took me 2 days to create due to its intense length, explanations, and the messy HTML code behind this blog haha.
Thank you for reading, and happy hacking!