Skip to content

An introduction to code de-obfuscation with Roblox Lua

Recently, I engaged in a discussion within my community about de-obfuscating Lua code. This blog post was requested after many were puzzled about how supposedly "random-looking" characters and invalid-looking syntax could possibly result in non-fault code being executed.

In this blog post, we will therefore examine a short code sample which will make the code in your brain start working, in order to potentially think about de-obfuscating larger scripts.

Although this post uses a small extract in Lua as an example, it is still relevant to other scripting/programming languages, as the same techniques of obfuscation are usually used.

So firstly, let us get the most basic thing out of the way:

What even is obfuscation?

In pure English definition, obfuscation (mass noun) 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. This can be done for a multitude of reasons, usually when an author wants to protect a valuable or proprietary implementation but is forced to distribute human-readable code (e.g., many Roblox scripts must be shared as plain code). Authors will garble identifiers, inline constants, or add convoluted logic to hide original formulas. However increasingly, this technique has also been used to make it harder for defenders to reverse-engineer genuinely malicious scripts.

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 eough effort, it can often be de-obfuscated (as will be demonstrated by this blog post).

Our starting point

The starting point
1
2
3
4
5
6
7
8
local t = tonumber
local _ = {1,{6,{7,{{{getfenv()[('t' .. 'a' .. 's' .. 'k')], 9}}}}}}[t(2)][t("2")][2][1][1][1].wait
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

The code above was actually used as a malicious troll by opening inappropriate links using a vulnerable Roblox service, but for the purpose of this blog post it has been adapted to be friendly.

At first glance, the code above may seem perplexing due to the string concatenations and inconsistencies in delimiters, the mixing of ASCII character codes with regular strings, and seemingly random and confusing mathematical expressions. However, by breaking it down line-by-line, we can uncover its hidden functionality.

Lines 1-4

Lines 1-2

local t = tonumber
local _ = {1,{6,{7,{{{getfenv()[('t' .. 'a' .. 's' .. 'k')], 9}}}}}}[t(2)][t("2")][2][1][1][1].wait

To a Lua programmer of any level, the indexing and nested literals look intentionally arcane, but it is straightforward once you break it down.

The code starts by aliasing tonumber to t so that calls like t("2") return the numeric index 2. The huge nested table literal is just a container filled with garbage values: by indexing into it with t("2") and other numeric keys, the code eventually reaches a value that is getfenv()[('t' .. 'a' .. 's' .. 'k')]. In other words, the expression builds the string "task" from 't' .. 'a' .. 's' .. 'k', looks that name up in the global environment returned by getfenv(), and then indexes its "wait" field.

Concretely, the whole expression is a delibereately obfuscated way to reference task.wait - Roblox's yielding function. It primarily relies on two ideas:

  1. dynamically constructing string keys (e.g. 't' .. 'a' .. 's' .. 'k'"task"), and
  2. using the global environment lookup via getfenv() or _G to map that string to the actual global table.

Therefore, the two lines above simply become local _ = task.wait.

Line 3

local __ = (getfenv()[string.char(103, 97, 109, 101)].LinkingService)

Once again, this line uses the same ideas that we mentioned in the previous two lines.

We can immediately omit the parenthesis surrounding the entire expression since it is completely useless and serves only as visual clutter. In short, (𝑥) ≡ 𝑥. This isn't even unique to programming, the same applies in mathematics too.

The actual interesting part here then simply becomes getfenv()[string.char(103, 97, 109, 101)].

Since we already discussed the role of getfenv(), I shall explain what string.char does in this instance. string.char is a standard Lua function which accepts a variable amount of numeric arguments representing an ASCII character to convert them into one string. Each number provided to the function here represents an ASCII character:

  • 103"g"
  • 97"a"
  • 109"m"
  • 101"e"

Thus, string.char(103, 97, 109, 101) returns "game".

Therefore our line has now transformed through this journey:

  1. local __ = getfenv()[string.char(103, 97, 109, 101)].LinkingService
  2. local __ = getfenv()["game"].LinkingService
  3. local __ = game.LinkingService

This de-obfuscation shows how ASCII values paired with environmenty lookups can be used to obscure code, making it less readable but still functionally equivalent.

Line 4

local ___ = (string[('r' .. 'e' .. 'v' .. 'e' .. 'r' .. 's' .. 'e')])

This is yet another classic case of string concatenations trying to make things less obvious, but by this point we are hopefully used to this kind of stuff.

The .. operator concatenates strings, so 'r' .. 'e' .. 'v' .. 'e' .. 'r' .. 's' .. 'e' evaluates to "reverse", and thus our line simply tries to access that field from Lua's string library. And once again, there are two useless parentheses in this line which are only used for syntactic bloating.

Substituting our findings, our line is now local ___ = string.reverse.

Reflecting on the variables

With out findings substituted, we now have much cleaner code that looks like this:

Cleaned up code with findings substituted
1
2
3
4
local t = tonumber
local _ = task.wait
local __ = game.LinkingService
local ___ = string.reverse

If you have understood everything so far, well done! And I am happy that at least one reader, you, has read all of this - considering most people's attention spans are really short these days.

But look where we are now! With the main variables and their values mapped out, we can now recognise the purpoise of each function or object in this code snippet.

Lines 5-8, the "while" loop

From hereon, we will dive into how the variables discussed previously are used in the script.

While loop section
1
2
3
4
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

Making this while loop simpler is just a matter of evaluating mathematical expressions in our head and just substituting the values of function returns back into this.

Line 5

while (0 == 0) do

This condition here, to really anyone, should be clear that it evaluates to true. This is because 0 will always be equal to 0, and when generalising: 𝑥 ≡ 𝑥.

Therefore this is just an infinite loop.

Line 6

_((5 ^ 2) ^ -2)

Here, we are passing a mathematical expression, (5 ^ 2) ^ -2, as an argument to _. Since we know that _ from Lines 1-2 is task.wait, we are simply calling it here.

Let us however break down the math before substituting it in:

In Lua, the ^ symbol represents the "to the power of" mathematical operation.

  1. 5^2 calculates to 25, since 5 to the power of 2 is the same as 5 ✕ 5, which is 25.
  2. Then 25^-2 is calculated, which mathematically requires us to first find 25^-1 and then square the result of that. This is because of the reciprocation rules in mathematics.
  3. To find 25^-1, we take the reciprocal of 25. A "reciprocal" is 1/𝑥, where 𝑥 is the number in question.
  4. 25^-1 is therefore the same as 1/25 which is 0.04 in decimal.
  5. Remember that we are actually finding 25^-2, so we have to square the answer to 25^-1: 0.04 ^ 2 == 0.04 * 0.04 == 0.0016.

You can read more about mathematical reciprocals on Wikipedia.

Thus, our simplified maths result is 0.0016. Knowing that _ is a function - indicated by the parenthesis after it and an argument - we are calling task.wait(0.0016).

This therefore gives us the following code, and we are very close to the end!

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

Line 7 - Embedded escape codes

Convert the embedded escape codes in the string back to regular characters using my online tool:

Using richy.lol/string-converter to convert embedded escape codes in Lua strings back to regular characters

and thus we now have this line:

Embedded escape codes replaced with regular characters
1
2
3
4
while true do
    task.wait(0.0016)
    __:openUrl("https://" .. ___("lol.yhcir"))
end

The final part

And remember that we previously discovered certain variables like __ and ___ to be the following:

Previously identified variables
1
2
3
4
local t = tonumber
local _ = task.wait
local __ = game.LinkingService
local ___ = string.reverse

Thus the following line: "https://" .. ___("lol.yhcir") simplifies to "https://" .. string.reverse("lol.yhcir"), which becomes "https://" .. "richy.lol" and finally "https://richy.lol".

Therefore, our final code just looks like this:

Manually de-obfuscated code
1
2
3
4
while true do
    task.wait(0.0016)
    game.LinkingService:openUrl("https://richy.lol")
end

Congratulations, you have just de-obfuscated a script with one of the best de-obfuscation tools known to man: your human brain.

What this code does is uses Roblox's LinkingService to open my website from a vulnerable environment every 0.0016 seconds.

Comments