.NET Code Protection in Practice: From Obfuscation to Virtual Machine Protection
Editar página.NET Code Protection in Practice: From Obfuscation to Virtual Machine Protection
Seção intitulada “.NET Code Protection in Practice: From Obfuscation to Virtual Machine Protection”This article explains how to implement a multi-layered code protection strategy in .NET projects, covering the full path from basic obfuscation to professional virtual machine protection.
Background
Seção intitulada “Background”In .NET application development, protecting core code such as license validation, business logic, and sensitive configuration from decompilation and reverse engineering is, frankly, a topic you cannot avoid. As the .NET ecosystem has matured, developers have gained access to a range of protection options, from built-in obfuscation attributes to professional virtualization-based protection tools.
As a complex multilingual monorepo project, HagiCode includes desktop applications, build systems, and license management capabilities. The code inevitably contains license validation logic, sensitive configuration such as API keys and product IDs, and business-critical logic. Those parts need serious protection, because no one wants their hard work to be exposed so easily.
This article shares the code protection approach we actually adopted in the HagiCode project and summarizes the full journey from early pitfalls to later optimization. Hopefully it gives you some useful ideas.
About HagiCode
Seção intitulada “About HagiCode”HagiCode is an open source AI coding assistant project dedicated to providing developers with an intelligent programming experience. The project uses a monorepo architecture and simultaneously maintains a VSCode extension, backend AI services, a cross-platform desktop client, and more. That multi-language, multi-platform complexity makes code protection an engineering challenge we have to face head-on.
The approach shared in this article is the result of real trial and error during HagiCode development. If you want to see how we solved these technical problems, keep reading. You may find a few unexpected takeaways.
Core Content
Seção intitulada “Core Content”1. Microsoft’s Built-in Obfuscation Attribute
Seção intitulada “1. Microsoft’s Built-in Obfuscation Attribute”.NET Framework provides a built-in [ObfuscationAttribute], which is the most basic and commonly used code obfuscation marker. This attribute lives in the System.Reflection namespace and allows you to apply baseline protection to code without introducing third-party tools.
Core features:
Featureproperty: Specifies the obfuscation feature, such as"ultra"(high obfuscation) or"all"(full obfuscation)Excludeproperty:truemeans exclude from obfuscation, andfalsemeans apply obfuscation- Can be applied to classes, methods, properties, and other type members
In the HagiCode project, you can see it used like this:
[Obfuscation(Feature = "ultra", Exclude = false)]public async Task<LicenseValidationResult?> ValidateLicenseAsync(...)The advantages of this approach are fairly obvious:
- No extra dependencies; it is built into .NET Framework out of the box
- Can be recognized and processed by third-party obfuscation tools
- Does not significantly increase the size of the compiled assembly
That said, it also has limitations. It is only a marker, and the actual obfuscation result depends on the tool implementation. It cannot provide virtual machine protection-level security.
2. VMP (Virtual Machine Protection)
Seção intitulada “2. VMP (Virtual Machine Protection)”VMP is a professional code protection tool that provides high-level protection by compiling code into virtual machine instructions. Unlike simple name obfuscation, VMP actually transforms code logic into a form that conventional decompilers cannot reconstruct.
Protection level classification:
| Level | Virtualization | Mutation | Anti-debugging | String Encryption | Use Cases |
|---|---|---|---|---|---|
| HIGH | full | high | enabled | enabled | License validation, session concurrency, sensitive constants |
| MEDIUM | partial | medium | enabled | enabled | Business logic, domain models |
| LOW | none | low | disabled | disabled | Utility classes, non-critical code |
The HagiCode project defines a declarative attribute system for marking code that needs protection:
// High-priority protection[VmProtect(VmProtectionPriority.High, Reason = "Contains license verification logic")]public class KeygenClient { ... }
// Exclude from protection[VmExclude(Reason = "Public API that must remain unchanged")]public class PublicApi { ... }
// Inherited protection[VmProtect(Priority.High, ProtectDerived = true)]public class BaseLicenseValidator { ... }3. Build-Time Protection Strategy
Seção intitulada “3. Build-Time Protection Strategy”VMP protection does not only matter at runtime. It also needs to be automated as part of the build pipeline, because doing it manually would be far too tedious. HagiCode’s build system supports several modes:
- Native Windows mode: Invoke the VMProtect tool directly
- Linux Docker container mode: Run VMP inside a container to solve cross-platform compatibility issues
- Attribute scanning: Automatically discover protection markers in code
- Validation mechanism: Confirm that protection has been applied successfully
Taken together, these capabilities make the process much easier to manage.
Solution
Seção intitulada “Solution”1. Using Microsoft’s Built-in Obfuscation Attribute
Seção intitulada “1. Using Microsoft’s Built-in Obfuscation Attribute”Apply ObfuscationAttribute directly in code:
using System.Reflection;
[Obfuscation(Feature = "ultra", Exclude = false)]public class LicenseService{ [Obfuscation(Feature = "ultra", Exclude = false)] public async Task<bool> ValidateLicenseAsync(string key) { // License validation logic }
[Obfuscation(Feature = "flow", Exclude = false)] private string DecryptToken(string encrypted) { // Decryption logic }}Sometimes you need to let test assemblies access internal members while still keeping production code secure:
[assembly: InternalsVisibleTo("HagiCode.Application.Tests")][assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for MoqThis makes testing much more convenient, because the code still needs to be tested properly.
2. Custom Attribute Definitions for VMP Protection
Seção intitulada “2. Custom Attribute Definitions for VMP Protection”Create custom protection attributes to control VMP behavior:
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property)]public class VmProtectAttribute : Attribute{ public VmProtectionPriority Priority { get; set; } public string? Reason { get; set; } public bool ProtectDerived { get; set; }}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property)]public class VmExcludeAttribute : Attribute{ public string? Reason { get; set; }}
public enum VmProtectionPriority{ None = 0, Low = 1, Medium = 2, High = 3}Custom attributes are often easier to work with because they reflect your own protection requirements directly.
3. VMP Configuration File
Seção intitulada “3. VMP Configuration File”protection: priority_mode: "attribute" # Attribute-based priority default_level: "medium"
tools: - name: "vmprotect" path: "C:\\Program Files\\VMProtect Ultimate\\VMProtect.exe"
protection_levels: high: virtualization: "full" mutation: "high" anti_debug: true anti_dump: true encrypt_strings: true encrypt_resources: true
medium: virtualization: "partial" mutation: "medium" anti_debug: true encrypt_strings: true
low: virtualization: "none" mutation: "low" anti_debug: falseA clearer configuration makes the system much easier to maintain later.
Practical Guide
Seção intitulada “Practical Guide”1. Protection Practices for Critical Components
Seção intitulada “1. Protection Practices for Critical Components”According to HagiCode’s code-protection specification, the following components must use HIGH-priority protection:
// Production constants - must be encrypted and protected by VMP[VmProtect(VmProtectionPriority.High, Reason = "Production constants")]public static class ProductionConstants{ // Encrypted string accessor, protected by VMP [VmProtect(VmProtectionPriority.High)] public static string GetLicenseServerUrl(IOptions<LicenseOptions> options) => ...;}
// License validation logic[VmProtect(VmProtectionPriority.High, Reason = "License verification logic")]public class KeygenClient : IKeygenClient{ [Obfuscation(Feature = "ultra", Exclude = false)] public async Task<LicenseValidationResult?> ValidateLicenseAsync(...) { ... }}
// Machine fingerprint service[VmProtect(VmProtectionPriority.High)]public class MachineFingerprintService : IMachineFingerprintService { ... }Critical code deserves stronger protection, because exposing core logic would cause real problems.
2. String Encryption and Runtime Decryption
Seção intitulada “2. String Encryption and Runtime Decryption”Encrypt strings at build time and decrypt them at runtime:
public static class StringDecryption{ [VmProtect(VmProtectionPriority.High, Reason = "CRITICAL SECURITY")] public static string DecryptString(byte[] encryptedData, byte[] key, byte[] iv) { using var aes = Aes.Create(); aes.Key = key; aes.IV = iv;
using var decryptor = aes.CreateDecryptor(); using var ms = new MemoryStream(encryptedData); using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); using var reader = new StreamReader(cs);
return reader.ReadToEnd(); }}
// Production constant accessor (lazy loading + caching)public static class ProductionConstants{ private static string? _cachedLicenseServerUrl;
public static string GetLicenseServerUrl(IOptions<LicenseOptions> options) { if (_cachedLicenseServerUrl == null) { var encrypted = GetEncryptedLicenseServerUrl();#if DEBUG _cachedLicenseServerUrl = options.Value.PrimaryServer.Url;#else _cachedLicenseServerUrl = StringDecryption.DecryptString( encrypted, GetEncryptionKey(), GetEncryptionIV());#endif } return _cachedLicenseServerUrl; }}This step matters because sensitive information should never be left in plaintext.
3. VMP Protection Verification
Seção intitulada “3. VMP Protection Verification”After the build, you must verify whether protection was applied successfully; otherwise, you cannot be sure it is actually working:
// Example verification scriptpublic bool VerifyProtection(string assemblyPath){ // 1. Check the VMP signature var bytes = File.ReadAllBytes(assemblyPath); var vmpSignature = Encoding.ASCII.GetBytes("VMProtect"); if (bytes.Any(b => vmpSignature.Contains(b))) { return true; }
// 2. Check for file size changes (the protected file is usually larger) var originalInfo = new FileInfo(assemblyPath.Replace(".dll", ".bak")); if (originalInfo.Exists) { var sizeRatio = (double)new FileInfo(assemblyPath).Length / originalInfo.Length; return sizeRatio > 1.1; }
return false;}Verification is always worth doing, because otherwise problems can slip through unnoticed.
4. Notes and Caveats
Seção intitulada “4. Notes and Caveats”There are several pitfalls here that deserve special attention:
-
Do not obfuscate all code: Public APIs, interface definitions, and DTO classes usually do not need protection. Excessive obfuscation can hurt performance and debugging. The HagiCode project learned this the hard way.
-
Protect key accessors: Methods that retrieve encryption keys must receive the same or a higher protection level than the encrypted data itself; otherwise the whole setup loses its value.
-
Balance testing and production: DEBUG builds should skip encryption to make development and debugging easier, while RELEASE builds should enable full protection. Remember to separate them with conditional compilation such as
#if DEBUG. -
Consider the Docker environment: Running VMP on Linux requires a containerized approach to ensure tool compatibility. HagiCode uses a Wine + VMP container solution to solve the cross-platform problem.
-
Verification is mandatory: After the build finishes, you must verify that protection was applied successfully. Otherwise sensitive code may still be exposed, and the verification code shown earlier exists for exactly this purpose.
Conclusion
Seção intitulada “Conclusion”With this multi-layer protection strategy, HagiCode built a comprehensive code security system that spans from baseline obfuscation to virtual machine protection:
- Layer 1: Use
ObfuscationAttributefor baseline marking and provide hints to third-party tools - Layer 2: Use custom
VmProtectAttributedeclarations to express protection intent and priority - Layer 3: Use VMP virtual machine protection to transform critical code into irreversible virtual machine instructions
- Layer 4: Automatically scan and apply protection during the build, then verify the result
This approach can resist ordinary decompilation tools while also standing up better against advanced reverse engineering attacks. If you are building a .NET application that needs code protection, I hope this gives you at least a useful reference point.
References
Seção intitulada “References”- VMProtect Official Documentation
- .NET ObfuscationAttribute Documentation
- HagiCode Project Homepage
- HagiCode Official Documentation
If this article helped you:
- Give us a Star on GitHub: github.com/HagiCode-org/site
- Visit the official website to learn more: hagicode.com
- Watch the 30-minute hands-on demo: www.bilibili.com/video/BV1pirZBuEzq/
- Try one-click installation: docs.hagicode.com/installation/docker-compose
- Quick installation for the Desktop app: hagicode.com/desktop/
- Public beta has started, and you are welcome to try it out