How to Reverse Engineer Custom DSL/VM Implementations with Switch-Case Opcode Patterns

Reverse engineering JavaScript-based DSL or virtual machine interpreters involves identifying the central IIFE structure, mapping the switch-case opcode dispatch loop, and instrumenting the execution flow to reconstruct the original logic.

Custom DSL and VM interpreters embedded in JavaScript often appear as heavily obfuscated Immediately-Invoked Function Expressions (IIFE) that dispatch operations through centralized switch-case opcode patterns. According to the zhaoxuya520/reverse-skill repository, these implementations follow predictable architectural signatures that enable systematic deobfuscation through three distinct analysis layers: identification, static analysis, and dynamic capture.

Identifying VM Signatures in Obfuscated JavaScript

Locate the tell-tale structural signatures documented in the repository’s 适用范围 (applicability scope) section. These VMs consistently exhibit specific code patterns that distinguish them from standard control-flow flattening obfuscation.

Key identification markers include:

  • IIFE with one-letter variables – The entire VM is wrapped in an IIFE containing dozens of single-letter identifiers (e.g., a, b, c, DG) to minimize code size and impede readability.
  • Central dispatch function – A primary dispatcher function, commonly named DG or similar, contains the core execution logic.
  • Switch-case opcode loop – The dispatcher implements a for loop with an embedded switch (aE) statement, where aE represents the current opcode variable read from a bytecode array.
  • Constant reference table – A constant pool such as C[9] stores strings, function indexes, and immediate values referenced throughout execution by numeric indices.

Analyzing the Switch-Case Opcode Dispatch

The VM’s execution semantics reside entirely within the switch-case block of the dispatch loop. Static analysis must focus on correlating each opcode value with its corresponding operation handler and operand resolution logic.

In the reference implementation pattern, the dispatcher evaluates bytecode through a construct similar to:

function DG() {
    for (;;) {
        var opcode = aE;  // Current bytecode instruction
        switch (opcode) {
            case 0x01: 
                // Push constant from C[9] onto stack
                break;
            case 0x02:
                // Binary operation on stack values
                break;
            case 0x03:
                // Jump or conditional branch
                break;
        }
    }
}

The aE variable typically reads sequentially from a hidden bytecode array, while operands resolve through lookups in the C[9] constant table. Map each case label to its semantic operation—arithmetic, logical, memory access, or control flow—to reconstruct the instruction set architecture.

Dynamic Capture and Execution Tracing

Static analysis alone cannot resolve dynamic jumps or runtime-generated code paths. Dynamic analysis requires instrumenting the central dispatch function to capture the live opcode stream and operand values during execution.

Instrument the VM by hooking the dispatcher to log execution state:

const original_DG = DG;
DG = function() {
    console.log(`[VM] Opcode: 0x${aE.toString(16)}, PC: ${program_counter}`);
    console.log(`[VM] Constants:`, C[9]);
    return original_DG.apply(this, arguments);
};

This instrumentation reveals the actual bytecode sequence and resolves indirect jumps that static analysis cannot predict. Capture the full execution trace to correlate switch-case entries with high-level operations and reconstruct the original program logic.

Practical Deobfuscation Workflow

Follow this systematic approach to reverse engineer switch-case VM implementations:

  1. Locate the IIFE entry point – Extract the outer closure and identify the initialization routine that populates the constant table C[9].

  2. Map the opcode handlers – Analyze the switch (aE) block within the DG function to create a lookup table correlating hex or numeric case values with JavaScript operations.

  3. Trace bytecode execution – Inject logging hooks into the dispatch loop to capture the opcode stream and stack operations during runtime.

  4. Reconstruct source logic – Translate the captured opcode trace back to semantically equivalent JavaScript, replacing VM stack operations with native variables and control structures.

Summary

  • Switch-case opcode VMs are encapsulated in IIFEs with single-letter variable names and a central dispatch function like DG.
  • The dispatch loop uses a for … switch (aE) pattern to decode bytecode instructions sequentially.
  • Constant pools such as C[9] store strings and function references accessed by index throughout execution.
  • Dynamic instrumentation of the dispatch function is required to resolve runtime control flow and reconstruct the original logic.

Frequently Asked Questions

What distinguishes switch-case VM obfuscation from control-flow flattening?

Switch-case VMs centralize execution in a single dispatch loop with a clear opcode-to-operation mapping, whereas control-flow flattening disperses logic across a state machine with indirect jumps. The VM pattern allows analysts to instrument one function (DG) to capture the entire instruction stream, making it more straightforward to reverse engineer than dispersed flattening schemes.

How do I identify the constant table in obfuscated JavaScript VMs?

Look for array initializers containing string literals and function references, typically assigned to short identifiers like C, c, or constants. In the zhaoxuya520/reverse-skill analysis pattern, the constant pool C[9] stores both data and code references, accessed via numeric indices within the switch (aE) opcode handlers.

Can I automate the decompilation of VM bytecode to original JavaScript?

Yes. By hooking the dispatch function and logging each switch case entry with its operands from C[9], you generate an execution trace that maps VM instructions back to high-level operations. This trace can be processed into semantically equivalent JavaScript, though meaningful variable names and comments require manual analysis.

Why do these implementations use single-letter variable names like aE and DG?

Single-letter identifiers result from aggressive minification and intentional obfuscation designed to reduce file size and impede static analysis. Despite the naming obfuscation, the VM architecture maintains consistent structural relationships: the dispatch loop, constant pool, and opcode handlers remain functionally distinct and analyzable.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →