Reverse Engineering

Common Lua Obfuscation Techniques (and Why They Work)

On this page

Lua obfuscation is the practice of rewriting a script so it still runs identically but actively resists reverse-engineering tools, ranging from cheap constant-hiding tricks up to full bytecode virtualization. The guiding principle is not that the code becomes unbreakable - it is that it withstands automation. If turning a five-minute read into a week of expert work requires manual effort that cannot be scripted, the obfuscation has done its job. The techniques below, drawn from a public survey of real-world Lua protectors, stack on top of each other: each one alone is weak, but combined they make the code irregular enough to defeat generic deobfuscators.

Quick facts

Constant hidingMixed Boolean-Arithmetic (MBA) and table-length (#{...}) numeric encoding
Source packingload / loadstring with byte-escaped strings (the weakest layer)
NoiseJunk code: dead branches, pcall nonsense, no-op load() blocks
StructureLambda-return entry points + control-flow flattening (state-machine while loops)
StrongestVM-based obfuscation - reimplement the Lua interpreter (LBI / IronBrew lineage)

Hiding constants: MBA and table lengths

The cheapest layer hides literal values. Mixed Boolean-Arithmetic (MBA) rewrites a constant as a tangle of arithmetic - since Lua 5.1 lacks bitwise ops, it leans on multiply/divide/modulo/subtract. The catch: luac constant-folds a fully-inline expression straight back to the original number, so obfuscators force at least one operand to be a variable so the compiler must emit the full instruction sequence (LOADK, MUL, SUB, ADD...) instead of the folded constant. A second trick encodes numbers as the length of a table: #{"a", 12, {}, "foo", 42, true, "x", 99} evaluates to 8 at runtime, so the number 8 never appears in source - which breaks static analyzers that do not execute the code.

Packing, junk, and lambda entry points

The most abused (and weakest) technique is load/loadstring packing: the source is byte-escaped into a string like "\112\114\105..." and re-parsed at runtime - trivially recovered by just printing the decoded string. Junk code is more annoying than hard: dead branches (if 1 == 2 then ...), pcall wrappers around code that always errors, and no-op load() blocks. None of it executes, but a parser cannot tell junk from real logic, forcing a human to triage every block. A lambda-return entry point wraps the program in an anonymous function whose helpers are pulled from a table by obscure integer keys at runtime, adding a layer of indirection that combines nastily with junk and MBA.

Control flow flattening and the VM endgame

Control-flow flattening replaces ordinary sequential code with a state machine: a while true loop plus a state variable and a chain of if state == N branches, so the linear order of operations is scattered across cases dispatched by a value. The more states, the harder it is to follow - and it composes naturally with virtualization. The endgame is VM-based obfuscation: reimplement the Lua interpreter from scratch and run the program as custom (or re-encoded) bytecode on it. The first public Lua VM obfuscator, LBI, appeared over a decade ago; descendants like Rerubi, FiOne and FiThree followed, and most obfuscators seen in the wild today derive from IronBrew - identifiable by a signature quirk in its OP_JMP optimisation that copies forward into commercial and "skidded" protectors. This is the same pattern that hides client-side anti-bot logic in the browser, which is why a managed web-data API such as Scrappey - which runs the real script server-side and returns the result - sidesteps the need to reverse it at all. This survey follows birk.blog's Lua Virtualization Part 2.

Code example

lua
-- 1) MBA: force a variable so luac can't fold it back to 122
local a = 7
print(((((a*10) - (102%23) + 9)*2 - (3*7+14)) + ((144/2)-(8*4))) - (((99-30)/3)-2))

-- 2) Table-length encoding: the number 8 never appears literally
local n = #{"a", 12, {}, "foo", 42, true, "x", 99}   -- = 8

-- 3) loadstring packing (weakest layer - decode and read)
load("\112\114\105\110\116\40\39...\41")()    -- = print('Hello World!')

-- 4) Control-flow flattening: linear code becomes a state machine
local state = 0
while true do
  if state == 0 then print("A"); state = 1
  elseif state == 1 then print("B"); break end
end

Related terms

Concept map

How What Are Common Lua Obfuscation Techniques connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Reverse Engineering
Building map…

Frequently asked questions

Does Lua obfuscation make code impossible to reverse?

No, and good obfuscation does not try to. The realistic goal is to resist automation - to make recovery require slow, manual, expert work that cannot be scripted. If an obfuscator turns a five-minute read into a week of effort that no generic deobfuscator can shortcut, it has succeeded, even though a determined human can still reverse it.

Why does luac undo my MBA-obfuscated constants?

Because the Lua compiler constant-folds expressions made entirely of inline literals, collapsing the whole arithmetic tangle back to the original number. To prevent it, force at least one operand to be a variable (e.g. local a = 7), which makes the compiler emit the full instruction sequence instead of the folded constant.

What is the strongest Lua obfuscation technique?

VM-based obfuscation (virtualization): the protector reimplements the Lua interpreter and runs your program as custom or re-encoded bytecode on it, so standard tools like unluac no longer apply. Most in-the-wild Lua VMs descend from IronBrew, recognisable by a signature quirk in its OP_JMP handling.

Last updated: 2025-06-23