How VM Obfuscation Works
The full pipeline transforms your original Lua source into a self-contained obfuscated file that embeds both the custom bytecode and the VM interpreter.1
Write Original Source
You write your Lua or Luau code normally. This is the logic you want to protect, such as license checks, decryption routines, or proprietary algorithms.
2
Compile to Custom Bytecode
A custom compiler translates your Lua source into a proprietary instruction set. This is your “bytecode” — a sequence of bytes or numbers that only your VM understands.
3
Encode the Bytecode
The custom bytecode is encoded as a Lua string, a table of bytes, or an encrypted blob. It is embedded directly into the output file alongside the VM.
4
Embed the VM Interpreter
The VM interpreter is written in Lua or Luau and embedded in the output file. It reads and executes your custom bytecode at runtime.
5
Runtime Execution
At runtime, the VM reads each custom instruction, decodes it, and performs the corresponding operation. The original logic never appears in standard Lua bytecode or source form.
Designing a Custom Instruction Set
The first step in building a VM is defining the instruction set. Your custom ISA must be capable of expressing all Lua operations that your original code uses.Stack-Based vs. Register-Based VMs
You can choose between two fundamental VM architectures: Stack-based VMs use a value stack. Instructions push operands onto the stack, perform operations on the top elements, and push results back. They are simpler to implement but slightly slower due to stack manipulation overhead.Minimum Viable Instruction Set
A register-based VM for Lua needs at least these instructions to be Turing-complete and Lua-compatible:Instruction Encoding
Each instruction can be encoded as 4 bytes:The VM Interpreter Loop
The VM interpreter is the core of the obfuscation. It fetches instructions, decodes them, and executes the corresponding operations.Complete Minimal Register-Based VM
Here is a complete minimal register-based VM in Lua. It supports LOAD_CONST, MOVE, ADD, SUB, JMP, CALL, and RETURN:VM Components Explained
Instruction Pointer (ip): Tracks the current position in the bytecode. After fetching a 4-byte instruction,ip advances by 4.
Register File (registers): A Lua table that acts as the VM’s register bank. Registers are indexed numerically starting from 0.
Constant Pool (constants): A table of literal values (numbers, strings, booleans, nil) referenced by index. The LOAD_CONST instruction copies constants into registers.
Call Stack (call_stack): Stores return addresses and register states for nested function calls. A production VM needs this for closures and recursive calls.
Encoding Custom Bytecode
You need a way to represent your custom instructions as data that can be embedded in a Lua file.Binary String Encoding
Usestring.char() to pack each instruction into 4 bytes:
Runtime Decoding
At runtime, extract bytes from the string:Flat Table Encoding
For simpler indexing, store instructions as a flat Lua table of numbers:Compiling Lua to Your Custom Bytecode
You need a compiler that translates Lua source into your custom instruction set. There are two approaches.Option A: Custom Compiler from AST
Write a compiler that parses Lua source into an AST and emits your instruction set. You can use an existing Lua parser library or write a recursive descent parser:Option B: Bytecode-to-ByteCode Translation
Reuse the standard Lua compiler but translate standard Lua bytecode to your custom bytecode. This is easier because the input format is well-defined:Obfuscating the VM Interpreter Itself
The VM interpreter is now the most valuable target for an analyst. You must obfuscate it just as aggressively as the original code.Apply Source-Level Obfuscation
Use the techniques from other sections of this guide on the VM interpreter:- Variable renaming: rename
registersto_,ipto__,opcodeto___ - String encoding: encode error messages and opcode names
- Control flow flattening: replace the if/elseif chain with a flattened dispatcher
Obfuscate Opcode Constants
The VM’s opcode numbers should not appear as plain literals. Use constant obfuscation:Function Dispatch Table
Replace the if/elseif chain with a function dispatch table. This is harder to analyze than a flat conditional chain:Flattened Dispatcher Version
For even stronger obfuscation, flatten the dispatcher so there is no obvious table lookup:Real-World Lua Virtualizers
Several open-source projects demonstrate production-quality Lua VM obfuscation.Ironbrew2
Ironbrew2 is a Lua 5.1 obfuscator focused on the Roblox ecosystem. It implements a register-based VM with the following features:- Custom instruction set with encrypted opcodes
- String encryption using XOR and dynamic keys
- Control flow obfuscation within the VM loop
- Anti-tamper checks that detect debugging attempts
Prometheus
Prometheus is a modular Lua 5.1 obfuscator with pluggable transformation steps. Its pipeline includes:- Lexical obfuscation (variable renaming, string encryption)
- Control flow flattening
- VM wrapping (converting functions to custom bytecode + VM)
- Anti-debug and anti-tamper layers
