跳转到内容

.NET Code Protection in Practice: From Obfuscation to Virtual Machine Protection

编辑此页
火山引擎 Coding Plan
火山引擎提供 Claude API 兼容服务,稳定可靠。订阅折上9折,低至8.9元,订阅越多越划算!
立即订阅
智谱 GLM Coding: 20+ 大编程工具无缝支持 推荐
Claude Code、Cline 等 20+ 大编程工具无缝支持,"码力"全开,越拼越爽!
立即开拼
MiniMax Claude API 兼容服务
MiniMax 提供 Claude API 兼容服务,支持多种模型接入,稳定可靠。
了解更多
阿里云千问 Coding Plan 上线
阿里云千问 Coding Plan 已上线,满足开发日常需求。推荐 + Hagicode,完美实现开发过程中的各项需求。
立即订阅

.NET Code Protection in Practice: From Obfuscation to Virtual Machine Protection

Section titled “.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.

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.

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.

1. Microsoft’s Built-in Obfuscation Attribute

Section titled “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:

  • Feature property: Specifies the obfuscation feature, such as "ultra" (high obfuscation) or "all" (full obfuscation)
  • Exclude property: true means exclude from obfuscation, and false means 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.

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:

LevelVirtualizationMutationAnti-debuggingString EncryptionUse Cases
HIGHfullhighenabledenabledLicense validation, session concurrency, sensitive constants
MEDIUMpartialmediumenabledenabledBusiness logic, domain models
LOWnonelowdisableddisabledUtility 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 { ... }

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.

1. Using Microsoft’s Built-in Obfuscation Attribute

Section titled “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:

AssemblyInfo.cs
[assembly: InternalsVisibleTo("HagiCode.Application.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq

This makes testing much more convenient, because the code still needs to be tested properly.

2. Custom Attribute Definitions for VMP Protection

Section titled “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.

vmp_config.yml
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: false

A clearer configuration makes the system much easier to maintain later.

1. Protection Practices for Critical Components

Section titled “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

Section titled “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.

After the build, you must verify whether protection was applied successfully; otherwise, you cannot be sure it is actually working:

// Example verification script
public 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.

There are several pitfalls here that deserve special attention:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

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 ObfuscationAttribute for baseline marking and provide hints to third-party tools
  • Layer 2: Use custom VmProtectAttribute declarations 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.


If this article helped you: