luac) and the runtime load() function both produce bytecode, which is a compact binary representation of your script. Because bytecode skips the source-level analysis step entirely, it is a natural target for obfuscation. An analyst who obtains your bytecode must first disassemble or decompile it before they can read the logic, and you can make that process significantly harder by stripping debug information and mutating the bytecode structure itself.
This page covers the Lua bytecode format, how to compile and load bytecode at runtime, how to strip debug metadata, and how to apply bytecode-level mutations that confuse standard disassemblers.
The Lua Bytecode Format
Standard Lua bytecode files (.luac) begin with a fixed header followed by one or more function prototypes. The header identifies the file as Lua bytecode and specifies the Lua version that produced it.
Standard Lua Bytecode Header
The header is always present at offset 0 and contains the following fields:
After the header comes the top-level function prototype, which contains:
- Instruction list: the actual VM instructions (32-bit fixed-size in most versions)
- Constants table: numeric, string, and boolean literals used by the code
- Upvalue list: upvalue descriptors
- Debug information: line number table, local variable names, upvalue names
- Nested function prototypes: each function in the file is a prototype
Lua Version Differences
Lua 5.1, 5.2, 5.3, and 5.4 each use slightly different bytecode formats. The version byte in the header is the primary discriminator:LuaJIT Bytecode
LuaJIT uses a completely different bytecode format (.ljc files). The header begins with 0x1B 0x4C 0x4A 0x02 (“\x1BLJ\x02”), and the instruction encoding is entirely different from standard Lua. LuaJIT bytecode is also not portable across architectures (32-bit vs 64-bit) or endianness.
Luau Bytecode
Luau (Roblox’s fork of Lua 5.1) uses a proprietary bytecode format that is not publicly documented. Roblox compiles Luau scripts server-side before sending them to clients. As a Roblox developer, you cannot access or deliver raw Luau bytecode to clients. The bytecode is internal to the Roblox engine and never exposed to developers.Compiling to Bytecode
You can produce bytecode using the command-line compiler or at runtime from within a running Lua script.Command-Line Compilation
The standard Lua compilerluac produces bytecode files:
0x1B 0x4C 0x75 0x61 at offset 0. You can verify this with xxd or a hex editor:
Runtime Compilation with string.dump
Inside a running Lua script,string.dump(f) returns the bytecode of a function f as a binary string:
string.dump controls debug stripping (see the Stripping Debug Information section).
Loading Bytecode at Runtime
Once you have bytecode as a string, you can load and execute it at runtime usingload (Lua 5.2+) or loadstring (Lua 5.1 compatibility):
Embedding Bytecode as a String Literal
Raw bytecode contains non-printable bytes, so you need to encode it before embedding it in a Lua source file. The two common approaches are escape encoding and base64 encoding.Escape Encoding with string.format
Base64 Encoding
Complete Loader Example
Here is a complete example that reads a Lua file, compiles it to bytecode, base64-encodes it, and generates a self-contained loader script:Stripping Debug Information
Debug information embedded in bytecode is a goldmine for reverse engineers. It includes local variable names, upvalue names, and line number mappings. Stripping this information makes decompiled output far less readable.What Debug Information Contains
- Line number table: maps each instruction back to a source line number
- Local variable list: names and scopes of all local variables
- Upvalue names: names of upvalues referenced by the function
Stripping with luac
Use the-s flag to strip debug information at compile time:
Stripping at Runtime
Passtrue as the second argument to string.dump:
What Gets Removed vs. What Remains
When you strip debug information:- Removed: local variable names (replaced with ”?” in most decompilers), upvalue names, line number table
- Remains: instruction opcodes, constants table, function structure, upvalue count
Hex Dump Comparison
In an unstripped bytecode file, the debug section contains the actual local names and line numbers. In a stripped file, these sections are present in the structure but contain zero-length or default values. A hex dump of the stripped file shows the debug section with empty strings and zeroed line number entries.Bytecode Mutation Techniques
Beyond stripping, you can mutate the bytecode itself to confuse disassemblers that rely on standard patterns.Constant Table Scrambling
The constants table stores all literals used by the code. You can reorder the constants and update everyLOADK instruction to reference the new indices:
NOP Instruction Insertion
Insert no-op instructions between real instructions. A safe NOP in Lua 5.1 isOP_MOVE where the source and destination registers are the same:
Fake Debug Names
Before stripping debug information, replace local and upvalue names with misleading values:Instruction Operand Obfuscation
Many Lua instructions have unused operand fields. For example, in Lua 5.1,OP_MOVE only uses the A and B fields; the C field is unused. You can fill unused fields with random values:
Protecting the Bytecode Loader
The loader script that decodes and executes bytecode is itself a target for analysis. If an analyst can find theload() call, they can hook it to intercept the decoded bytecode.
Minimal Obfuscated Loader
Here is a loader that decodes base64, applies a simple XOR, and then callsload():
Luau (Roblox) developers cannot access or deliver raw bytecode to clients. Roblox compiles Luau server-side, and the bytecode is internal to the engine. The techniques on this page apply to standalone Lua and LuaJIT deployments.
What tools can disassemble Lua bytecode?
What tools can disassemble Lua bytecode?
Several tools can reverse standard Lua bytecode, which is why bytecode alone is insufficient protection:
- ChunkSpy: A Lua 5.1 bytecode disassembler with detailed annotation
- luadec: A Lua 5.1 and 5.2 decompiler that reconstructs source code from bytecode
- unluac: A decompiler supporting all Lua versions from 5.0 through 5.4
- LuaJIT
-bl: The built-in bytecode listing tool for LuaJIT
