Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pw4k/ironbrew-2/llms.txt

Use this file to discover all available pages before exploring further.

Overview

IronBrew 2 employs multiple layers of obfuscation to protect Lua code. These techniques work together to make reverse engineering extremely difficult.

Obfuscation Settings

Obfuscation is controlled via ObfuscationSettings (ObfuscationSettings.cs:3-31):
public class ObfuscationSettings
{
    public bool EncryptStrings;           // Encrypt all strings
    public bool EncryptImportantStrings;  // Encrypt specific strings
    public bool ControlFlow;              // Enable control flow obfuscation
    public bool BytecodeCompress;         // Compress bytecode
    public int DecryptTableLen;           // String decryption table size
    public bool PreserveLineInfo;         // Keep line info (debug)
    public bool Mutate;                   // Enable instruction mutation
    public bool SuperOperators;           // Enable super operators
    public int MaxMiniSuperOperators;     // Mini super operator count
    public int MaxMegaSuperOperators;     // Mega super operator count
    public int MaxMutations;              // Maximum mutations
}
Default configuration:
  • String encryption: disabled
  • Control flow: enabled
  • Bytecode compression: enabled
  • Mutations: enabled (max 200)
  • Super operators: enabled (120 mini, 120 mega)

Instruction Mutations

What Are Mutations?

Mutations create alternative register orderings for each instruction (Generator.cs:100-126):
public List<OpMutated> GenerateMutations(List<VOpcode> opcodes)
{
    Random r = new Random();
    List<OpMutated> mutated = new List<OpMutated>();

    foreach (VOpcode opc in opcodes)
    {
        if (opc is OpSuperOperator)
            continue;

        // Create 35-50 mutations per opcode
        for (int i = 0; i < r.Next(35, 50); i++)
        {
            int[] rand = {0, 1, 2};
            rand.Shuffle();

            OpMutated mut = new OpMutated();
            mut.Registers = rand;  // Shuffled register order
            mut.Mutated = opc;
            
            mutated.Add(mut);
        }
    }

    mutated.Shuffle();
    return mutated;
}

How Mutations Work

For a standard instruction like ADD R(A) R(B) R(C): Original:
local OP_A = Inst[2];
local OP_B = Inst[3];
local OP_C = Inst[4];
Stack[OP_A] = Stack[OP_B] + Stack[OP_C];
Mutated (registers shuffled to [1, 0, 2]):
local OP_B = Inst[2];  -- Now reads from position A
local OP_A = Inst[3];  -- Now reads from position B
local OP_C = Inst[4];  -- Still position C
Stack[OP_A] = Stack[OP_B] + Stack[OP_C];  -- Same logic
The instruction encoding is adjusted to match:
  • If registers are [1, 0, 2], the serializer swaps A and B positions
  • Each instruction instance can use a different mutation
  • Makes pattern recognition much harder

Mutation Folding

Mutations are applied to actual instructions (Generator.cs:128-167):
public void FoldMutations(List<OpMutated> mutations, HashSet<OpMutated> used, Chunk chunk)
{
    for (int i = 0; i < chunk.Instructions.Count; i++)
    {
        Instruction opc = chunk.Instructions[i];
        CustomInstructionData data = opc.CustomData;
        
        foreach (OpMutated mut in mutations)
            if (data.Opcode == mut.Mutated && data.WrittenOpcode == null)
            {
                if (!used.Contains(mut))
                    used.Add(mut);

                data.Opcode = mut;  // Apply mutation
                break;
            }
    }
}
Only mutations that are actually used get included in the final VM.

Super Operators

Concept

Super operators combine multiple instructions into a single virtual opcode. Instead of executing 5 separate instructions, one super operator executes all 5 at once.

Types of Super Operators

Mini Super Operators

Combine 5-10 consecutive instructions (Generator.cs:392-399):
var miniOperators = GenerateSuperOperators(_context.HeadChunk, 10)
    .OrderBy(t => r.Next())
    .Take(settings.MaxMiniSuperOperators)
    .ToList();

virtuals.AddRange(miniOperators);
FoldAdditionalSuperOperators(_context.HeadChunk, miniOperators, ref folded);

Mega Super Operators

Combine 60-80 consecutive instructions (Generator.cs:383-390):
var megaOperators = GenerateSuperOperators(_context.HeadChunk, 80, 60)
    .OrderBy(t => r.Next())
    .Take(settings.MaxMegaSuperOperators)
    .ToList();

virtuals.AddRange(megaOperators);
FoldAdditionalSuperOperators(_context.HeadChunk, megaOperators, ref folded);

Generation Process

Super operators are generated by scanning for sequences (Generator.cs:169-256):
public List<OpSuperOperator> GenerateSuperOperators(Chunk chunk, int maxSize, int minSize = 5)
{
    List<OpSuperOperator> results = new List<OpSuperOperator>();
    bool[] skip = new bool[chunk.Instructions.Count + 1];

    // Mark instructions that can't be combined
    for (int i = 0; i < chunk.Instructions.Count - 1; i++)
    {
        switch (chunk.Instructions[i].OpCode)
        {
            case Opcode.Jmp:
            case Opcode.ForLoop:
            case Opcode.ForPrep:
                skip[i + 1] = true;  // Can't combine after jumps
                break;
            
            case Opcode.Test:
            case Opcode.TestSet:
                skip[i + 1] = true;  // Can't combine after tests
                break;
        }
    }
    
    // Scan for valid sequences
    int c = 0;
    while (c < chunk.Instructions.Count)
    {
        int targetCount = maxSize;
        OpSuperOperator superOperator = new OpSuperOperator {SubOpcodes = new VOpcode[targetCount]};

        // Check if we can take targetCount instructions
        for (int j = 0; j < targetCount; j++)
        {
            if (c + j > chunk.Instructions.Count - 1 || skip[c + j])
            {
                // Reduce size if we hit a boundary
                targetCount = j;
                break;
            }
        }

        if (targetCount >= minSize)
        {
            // Create super operator from this sequence
            for (int j = 0; j < targetCount; j++)
                superOperator.SubOpcodes[j] = chunk.Instructions[c + j].CustomData.Opcode;

            results.Add(superOperator);
        }
        
        c += targetCount + 1;
    }

    return results;
}

Super Operator Code Generation

Super operators generate inline code (OpSuperOperator.cs:36-69):
public override string GetObfuscated(ObfuscationContext context)
{
    string s = "";
    List<string> locals = new List<string>();
    
    for (var index = 0; index < SubOpcodes.Length; index++)
    {
        var subOpcode = SubOpcodes[index];
        string s2 = subOpcode.GetObfuscated(context);
        
        // Extract local declarations
        Regex reg = new Regex("local(.*?)[;=]");
        foreach (Match m in reg.Matches(s2))
        {
            string loc = m.Groups[1].Value.Replace(" ", "");
            if (!locals.Contains(loc))
                locals.Add(loc);
            
            // Remove duplicate declarations
            if (!m.Value.Contains(";"))
                s2 = s2.Replace($"local{m.Groups[1].Value}", loc);
            else 
                s2 = s2.Replace($"local{m.Groups[1].Value};", "");
        }

        s += s2;

        // Advance to next instruction
        if (index + 1 < SubOpcodes.Length)
            s += "InstrPoint = InstrPoint + 1;Inst = Instr[InstrPoint];";
    }

    // Hoist all locals to the top
    foreach (string l in locals)
        s = "local " + l + ';' + s;
        
    return s;
}
Result: Instead of 10 separate opcode dispatches, one dispatch executes all 10 inline.

String Encryption

XOR-Based Encryption

String encryption uses XOR with a random table (ConstantEncryption.cs:17-27):
public string Encrypt(byte[] bytes)
{
    List<byte> encrypted = new List<byte>();
    int L = Table.Length;
    
    for (var index = 0; index < bytes.Length; index++)
        encrypted.Add((byte)(bytes[index] ^ Table[index % L]));
    
    // Returns inline Lua decryption code
    return $"((function(b)...xor decryption...end)(\"...encrypted data...\"))";
}

Decryption Table Generation

Each string gets a random XOR table (ConstantEncryption.cs:29-36):
public Decryptor(string name, int maxLen)
{
    Random r = new Random();
    Name = name;
    Table = Enumerable.Repeat(0, maxLen)
        .Select(i => r.Next(0, 256))
        .ToArray();
}

String Encryption Modes

Mode 1: Encrypt All Strings

When EncryptStrings = true (ConstantEncryption.cs:140-165):
Regex r = new Regex(encRegex, RegexOptions.Singleline);
var matches = r.Matches(_src);

Decryptor dec = GenerateGenericDecryptor(matches);

foreach (Match m in matches)
{
    string captured = m.Groups[2].Value + m.Groups[4].Value;
    string nStr = before + dec.Encrypt(UnescapeLuaString(captured));
    // Replace string with encrypted version
}
All strings use the same decryptor for efficiency.

Mode 2: Encrypt Marked Strings

When EncryptStrings = false but EncryptImportantStrings = true (ConstantEncryption.cs:167-196):
foreach (Match m in matches)
{
    string captured = m.Groups[2].Value + m.Groups[4].Value;
    
    if (!captured.StartsWith("[STR_ENCRYPT]"))
        continue;  // Only encrypt marked strings

    captured = captured.Substring(13);
    Decryptor dec = new Decryptor("IRONBREW_STR_ENCRYPT" + n++, m.Length);
    // Each marked string gets its own decryptor
}
Mark strings with "[STR_ENCRYPT]your text" to encrypt them.

Mode 3: Important String Detection

When EncryptImportantStrings = true (ConstantEncryption.cs:198-239):
List<string> sTerms = new List<string>() {"http", "function", "metatable", "local"};

foreach (Match m in matches)
{
    string captured = m.Groups[2].Value + m.Groups[4].Value;
    
    bool cont = false;
    foreach (string search in sTerms)
    {
        if (captured.ToLower().Contains(search.ToLower()))
            cont = true;
    }

    if (!cont)
        continue;  // Only encrypt important strings
    
    // Encrypt with unique decryptor
}
Automatically encrypts strings containing sensitive keywords.

Bytecode Compression

LZW Compression

Bytecode can be compressed using LZW (Generator.cs:41-72):
public static List<int> Compress(byte[] uncompressed)
{
    Dictionary<string, int> dictionary = new Dictionary<string, int>();
    for (int i = 0; i < 256; i++)
        dictionary.Add(((char)i).ToString(), i);
 
    string w = string.Empty;
    List<int> compressed = new List<int>();
 
    foreach (byte b in uncompressed)
    {
        string wc = w + (char)b;
        if (dictionary.ContainsKey(wc))
            w = wc;
        else
        {
            compressed.Add(dictionary[w]);
            dictionary.Add(wc, dictionary.Count);
            w = ((char)b).ToString();
        }
    }
 
    return compressed;
}

Base-36 Encoding

Compressed data is encoded in base-36 to embed in Lua (Generator.cs:74-98):
public static string ToBase36(ulong value)
{
    const string base36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    var sb = new StringBuilder(13);
    do
    {
        sb.Insert(0, base36[(byte)(value % 36)]);
        value /= 36;
    } while (value != 0);
    return sb.ToString();
}

public static string CompressedToString(List<int> compressed)
{
    StringBuilder sb = new StringBuilder();
    foreach (int i in compressed)
    {
        string n = ToBase36((ulong)i);
        sb.Append(ToBase36((ulong)n.Length));  // Length prefix
        sb.Append(n);                          // Value
    }
    return sb.ToString();
}
Result: Bytecode string is significantly smaller and harder to read.

Randomization Techniques

Chunk Step Shuffling

The order of chunk components is randomized (ObfuscationContext.cs:55-56):
ChunkSteps = Enumerable.Range(0, (int)ChunkStep.StepCount)
    .Select(i => (ChunkStep)i).ToArray();
ChunkSteps.Shuffle();
Possible orders:
  • Parameters → Instructions → Functions → LineInfo
  • Instructions → Functions → Parameters → LineInfo
  • Functions → LineInfo → Instructions → Parameters
  • … (24 permutations)

Constant Type Remapping

Constant type IDs are shuffled (ObfuscationContext.cs:64-65):
ConstantMapping = Enumerable.Range(0, 4).ToArray();
ConstantMapping.Shuffle();
Instead of standard mapping (0=nil, 1=bool, 3=number, 4=string):
  • Could be: 2=nil, 0=bool, 3=number, 1=string
  • Makes constant table harder to parse

Opcode Shuffling

Virtual opcodes are shuffled (Generator.cs:404-407):
virtuals.Shuffle();

for (int i = 0; i < virtuals.Count; i++)
    virtuals[i].VIndex = i;
Opcode indices are completely randomized each obfuscation.

XOR Key Randomization

New XOR keys for each obfuscation (ObfuscationContext.cs:67-71):
Random rand = new Random();
PrimaryXorKey = rand.Next(0, 256);
IXorKey1 = rand.Next(0, 256);
IXorKey2 = rand.Next(0, 256);

Layer Combination

All techniques work together:
  1. Bytecode is deserialized from Lua 5.1 format
  2. Control flow is obfuscated (see Control Flow)
  3. Instructions are mapped to virtual opcodes
  4. Mutations are generated (35-50 per opcode)
  5. Super operators are created (up to 240 total)
  6. Mutations are folded into instructions
  7. Super operators are folded to combine sequences
  8. Opcodes are shuffled for random indices
  9. Bytecode is serialized with randomized chunk steps
  10. Bytecode is XOR encrypted with random key
  11. Bytecode is compressed (optional)
  12. Strings are encrypted (optional)
  13. VM is generated with binary search dispatch
Result: Every obfuscation is unique and extremely difficult to reverse engineer.

Performance Considerations

When to Use Each Technique

Always use:
  • VM-based obfuscation (core feature)
  • Bytecode XOR encryption (minimal overhead)
  • Chunk step shuffling (no runtime cost)
Use for strong protection:
  • Instruction mutations (moderate overhead)
  • Mini super operators (improves performance)
  • Bytecode compression (reduces size)
Use for maximum protection:
  • Mega super operators (can reduce dispatch overhead)
  • String encryption (adds decryption overhead)
  • Control flow obfuscation (significant complexity)
Avoid in performance-critical code:
  • Excessive mega super operators
  • Encrypting all strings
  • Maximum mutation count

Build docs developers (and LLMs) love