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.