OP_MOVE is opcode 0 in Lua 5.1, OP_LOADK is opcode 1, and so on. Because these numbers are public and unchanging, any tool that reads Lua bytecode can immediately interpret the instruction stream. By remapping opcodes to different values and patching the Lua virtual machine to understand your custom numbering, you produce bytecode that standard tools cannot decode without first reverse engineering your VM modifications.
This page explains how Lua opcodes work, how to remap them by modifying the Lua interpreter source, how to shuffle operand fields, and how to distribute a custom Lua runtime alongside your protected bytecode.
How Lua Opcodes Work
Lua instructions are 32-bit fixed-size values in most Lua versions. The bits are divided into an opcode field and operand fields that vary by instruction type.Instruction Layout
In Lua 5.1, the instruction format uses the following bit layout:
For instructions that need larger constants, the B and C fields can be combined into a single 18-bit
Bx field, or a signed sBx field for jump offsets.
Standard Lua 5.1 Opcode Table
Lua 5.1 defines 38 opcodes numbered 0 through 37:
Lua 5.4 expands this to 83 opcodes with new instructions for integer operations, constant table improvements, and more efficient closures.
The Remapping Technique
Opcode remapping requires modifying the Lua interpreter source code, recompiling both the compiler (luac) and the runtime (lua), and then using your custom toolchain to produce and execute bytecode.
Step 1: Modify the Opcode Enum
The opcode definitions live inlopcodes.h. In Lua 5.1, the enum looks like this:
Step 2: Recompile the Toolchain
After modifyinglopcodes.h, rebuild both the compiler and the interpreter:
luac now emits bytecode with your custom opcode values. Your modified lua interpreter knows how to execute them. Standard luac and lua binaries will not be able to read or run your bytecode.
Step 3: Compile Scripts with Your Custom luac
unluac reads your protected.luac, it interprets opcode 17 as OP_MOVE (which is correct in your mapping but wrong in standard Lua). Since standard tools expect opcode 0 for OP_MOVE, they misinterpret the entire instruction stream and produce garbage output.
Shuffle vs. Encrypt the Dispatch Table
A more aggressive variant goes beyond static remapping and adds runtime decoding to the VM’s instruction dispatch loop.Static Remapping vs. Dynamic Decoding
Static remapping (the technique above) assigns fixed new numbers to each opcode. Dynamic decoding adds a translation step inside the VM loop:Runtime-Keyed Decode Tables
You can key the decode table by a hash of the script content, so different scripts use different opcode mappings:Operand Field Shuffling
In addition to opcode remapping, you can rearrange the bit fields within the 32-bit instruction word.Standard vs. Shuffled Bit Layout
Standard Lua 5.1 layout:Modifying the Bit Extraction Macros
The bit manipulation macros inlopcodes.h control how the compiler encodes and the VM decodes instructions:
POS_A, POS_B, POS_C, and POS_OP:
luac (the compiler) and lvm.c (the VM) must use identical macros. If they differ by even one bit position, every instruction will be decoded incorrectly and the VM will crash.
Distribution Implications
Opcode remapping requires distributing a custom Lua VM alongside your bytecode. This has important practical implications.Standalone Lua Applications
For desktop or server applications, package your modifiedlua executable or liblua.so with your application:
Embedded Lua in Game Engines
Many game engines embed Lua for scripting. To use opcode remapping, you must modify the embedded Lua source and recompile the entire engine:- Locate the Lua source inside the engine (often
ThirdParty/lua/orexternal/lua/) - Apply your
lopcodes.handlvm.cmodifications - Rebuild the engine
- Compile game scripts with your custom
luac
Roblox Luau Limitations
In Roblox Luau, you cannot replace the VM. The Luau interpreter is built into the Roblox client and server, and developers have no access to modify it. Opcode remapping is not possible in Roblox. For Roblox, use VM-based obfuscation implemented in Luau (see the Virtualization page) instead.Partial Opcode Remapping Without Recompiling the VM
If you cannot replace the VM, you can still achieve opcode remapping by implementing a custom VM interpreter in Lua itself. Your custom bytecode uses your own opcode numbers, and a Lua-written VM decodes and executes them. This approach is what VM-based obfuscation does at its core. Instead of patching the C VM, you write a Lua VM in Lua:This technique is what serious commercial game protection tools do under the hood. It is the most robust protection technique that does not require building a full custom VM from scratch.
