Skip to content

Feature: method virtualization (VM-based protection) #111

Description

@mcpolo99

Summary

Add method virtualization — translate selected methods' CIL into a custom bytecode executed by an embedded interpreter (VM), so the original IL never exists in the output assembly. This is the strongest form of .NET code protection and the natural high-end complement to our existing protections.

It also fills the conceptual gap left by removing the broken JIT anti-tamper mode (#110): where JIT tried (and failed, see #22) to keep method bodies hidden via a fragile native JIT hook, virtualization achieves the same goal — original code never materializes — in pure managed code, cross-runtime, with no native hooking.

Motivation

  • Strongest protection tier. Unlike encrypt-at-rest anti-tamper (which decrypts full method bodies into memory at load), a virtualized method's original IL is gone: a decompiler sees only the VM interpreter loop plus opaque bytecode. No public spec, no decompiler support.
  • Replaces the JIT concept properly. The JIT hook was removed because it is unmaintainable on modern .NET (version-locked JIT-EE interface, tiered compilation, ReadyToRun, NativeAOT) and never actually hid bodies (JIT AntiTamper does not actually hide method bodies (upstream #102) #22). A managed VM sidesteps all of that.
  • Synergy with the identity work (Unique Obfuscation Identity System — per-build and per-project fingerprint elimination #69). The VM's opcode↔handler mapping can be randomized per build, so every protected assembly ships a different VM. That is the same per-build-uniqueness principle as the Level-2 constant randomization, applied to the whole instruction set — extremely hostile to generic devirtualizers.

Prior art

  • KoiVM — the open-source ConfuserEx virtualization plugin (originally by Sq/"Koi", later open-sourced; many forks: DarksVM, TheProxyRE/KoiVM, oddmario/KoiVM). It is the direct precedent: a register-based VM (R0–Rn + stack + flags, which resists devirtualization better than a stack VM), shipped as a ConfuserEx plugin — a translator (CIL → "Koi" bytecode stream) plus an injected runtime interpreter (RT), with a method's body replaced by a call into the VM entry with a pointer into the stream.
    • Known limits we would inherit/design around: no calli/jmp (and a few others), open generics are hard, ref-structs (Span<T>) conflict with the VM stack model, a documented memory-leak risk on heavily-looped virtualized methods, large performance penalty (→ selective use), and original support limited to .NET Framework + CoreCLR on Windows — not validated on .NET 5–10.
  • Commercial references for scope/behavior: Eazfuscator.NET, .NET Reactor, ByteHide — all offer selective code virtualization with the same tradeoffs.

Proposed architecture (this fork)

  1. Translator (a new protection phase): lift each targeted method's CIL to a register-based IR, then emit our bytecode. Operands (tokens, constants) encoded/encrypted.
  2. Runtime interpreter (injected stub, must run net20 → net10): a dispatch loop over the bytecode with handlers for each virtual opcode; bridges back to the CLR for call/newobj/field access via metadata tokens.
  3. Per-method opt-in: given the perf cost, virtualization is applied only to methods the user selects (rule/attribute), like every other protection here.
  4. Per-build VM randomization (hardening): shuffle the opcode↔handler map and encode constants per build (ties into Unique Obfuscation Identity System — per-build and per-project fingerprint elimination #69).

Cross-framework & hard problems (must be scoped honestly)

Area Concern
Runtimes The interpreter is pure managed, so it can target net20 → net10 — but it must avoid APIs missing on old TFMs and must be tested on each.
NativeAOT / trimming / single-file A token-driven VM relies on metadata/reflection-ish bridging; will not work under full AOT/trimming. Document as unsupported.
Exception handling Mapping try/catch/finally/filter regions into the VM is the single hardest part (EH metadata, unwinding, rethrow). Likely phase 2.
Generics Open generic methods can't be virtualized cleanly; closed instantiations are OK.
ref structs Span<T>/ref locals conflict with a boxed VM stack model — exclude.
Unsupported opcodes calli, jmp, arglist, tail calls, etc. — fall back to leaving those methods un-virtualized.
Perf / memory Interpretation overhead + KoiVM's known leak pattern → selective use + careful handler lifetime.
Debuggability Stack traces/PDBs across the VM boundary (KoiVM exposes stackwalk/dbgInfo toggles for this).

Suggested phased plan

  • Phase 0 — decision: modernize/vendor an open-source KoiVM (ConfuserEx-native, big head start, but old and messy) vs. build a fresh register VM against our dnlib/runtime stack. Prototype devirtualization resistance + perf on a sample.
  • Phase 1 — core: virtualize straight-line + branching + arithmetic + call/field/ldstr for non-generic, EH-free methods, net48 + one modern TFM. Round-trip test (virtualize → run → assert).
  • Phase 2 — coverage: exception handling, closed generics, more opcodes; broaden TFM matrix.
  • Phase 3 — hardening: per-build opcode randomization, constant encryption, anti-devirt tricks (opaque predicates, handler duplication).

Non-goals / risks

  • Not a substitute for the working normal/anti anti-tamper — it's an additional, heavier tier.
  • Large, ongoing subsystem; real performance cost; AOT-incompatible. Worth it as a headline capability, but should land incrementally behind opt-in.

Acceptance for an MVP (Phase 1)

  • A method marked for virtualization has no original CIL in the output, runs correctly on net48 + a modern TFM, and survives a round-trip integration test. de4dot/decompilers show only the interpreter + bytecode.

Related: #110 (removed the broken JIT mode this supersedes), #22 (why the JIT approach failed), #69 (per-build uniqueness the VM randomization would extend).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds-evaluationRequires further analysis before implementationruntimeRuntime stubs and protections

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions