You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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.
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.
Per-method opt-in: given the perf cost, virtualization is applied only to methods the user selects (rule/attribute), like every other protection here.
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).
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).
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
Prior art
RT), with a method's body replaced by a call into the VM entry with a pointer into the stream.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.Proposed architecture (this fork)
call/newobj/field access via metadata tokens.Cross-framework & hard problems (must be scoped honestly)
try/catch/finally/filterregions into the VM is the single hardest part (EH metadata, unwinding, rethrow). Likely phase 2.Span<T>/reflocals conflict with a boxed VM stack model — exclude.calli,jmp,arglist, tail calls, etc. — fall back to leaving those methods un-virtualized.stackwalk/dbgInfotoggles for this).Suggested phased plan
call/field/ldstrfor non-generic, EH-free methods, net48 + one modern TFM. Round-trip test (virtualize → run → assert).Non-goals / risks
normal/antianti-tamper — it's an additional, heavier tier.Acceptance for an MVP (Phase 1)
Related: #110 (removed the broken JIT mode this supersedes), #22 (why the JIT approach failed), #69 (per-build uniqueness the VM randomization would extend).