Native AOT vs .NET Obfuscation: What Each Actually Protects
Native AOT compiles your .NET application to a self-contained native binary before deployment. No IL, no JIT, no .NET runtime required on the target machine. For many developers, the natural follow-up question is: does this mean my code is protected?
The honest answer is: partially, and not by design.
What Native AOT was built to do
Native AOT was introduced in .NET 7 and stabilized in .NET 8 as a performance and deployment feature, not a security feature. The goals were faster startup times, lower memory usage, smaller deployments, and the ability to ship a self-contained executable without requiring the .NET runtime to be installed.
Any protection Native AOT provides against reverse engineering is a side-effect of compilation, not an intentional design decision. The team that built Native AOT was not thinking about obfuscation. They were thinking about startup latency and deployment size.
This distinction matters because it shapes what the protection looks like in practice - and what it does not cover.
What Native AOT actually protects against
Compared to a standard .NET assembly, a Native AOT binary is meaningfully harder to reverse engineer:
No IL to decompile. Standard decompilers like ILSpy and dnSpy cannot open a Native AOT binary. The code has been compiled to native machine instructions for the target platform. An attacker who opens the binary in a standard .NET decompiler sees nothing useful.
No CIL metadata in the standard form. The richly structured metadata tables that make .NET assemblies self-documenting - TypeDef, MethodDef, FieldDef - are not present in the same form. The code structure that makes unprotected .NET assemblies nearly readable as source code is gone.
Reverse engineering requires native analysis skills. Instead of a .NET decompiler, an attacker needs native code analysis tools: Ghidra, IDA Pro, Binary Ninja. This is a higher skill requirement than opening a DLL in ILSpy.
These are real barriers. Native AOT does raise the reverse engineering cost compared to unprotected managed code.
What Native AOT does not protect
The barriers above are genuine but incomplete. Several things remain accessible even in a Native AOT binary.
Symbol files are produced by default and are highly readable. Native AOT compilation produces debug symbol files alongside the binary: .pdb on Windows, .dbg on Linux, .dSYM on macOS. Microsoft explicitly documents these as necessary for debugging and crash-dump inspection. These symbol files contain line-number information, local variable names, parameter names, and type information - a detailed map of the application's structure that makes native analysis significantly easier.
If you ship symbol files alongside your Native AOT binary, an attacker has access to far more information than the binary alone. On Unix-like systems, setting StripSymbols=false in the project file embeds the symbols directly inside the binary, increasing size while making the binary more self-descriptive. For any application where security is a concern, symbol files should be treated as sensitive artifacts - stored separately, not distributed with the application.
Reflection metadata is still present. Native AOT requires reflection metadata to support .NET runtime features - exception handling, stack traces, and any code that uses reflection at runtime. This metadata includes type names, method names, and field names. The exact amount depends on trimming configuration, but a significant portion of your application's symbol information survives into the native binary.
Native analysis tools are mature and widely available. Ghidra is free, open-source, and capable. IDA Pro is the industry standard for native binary analysis. Both handle x64, ARM64, and other architectures that .NET Native AOT targets. An attacker who knows what they are looking for does not need .NET-specific skills - native reverse engineering is a well-understood discipline with decades of tooling behind it.
String literals are still visible. String data in a native binary is stored in the binary's data sections and remains readable with basic tools. API keys, connection strings, internal URLs, license validation strings - all still present as plaintext unless separately encrypted before compilation.
The logical structure is recoverable. Native code analysis is harder than IL decompilation, but it is not impossible. A patient analyst with Ghidra and the reflection metadata as a guide can reconstruct the logical structure of the application. It takes longer than opening a DLL in ILSpy. It is not as clean. But it is achievable for a motivated, skilled attacker.
The IL2CPP parallel
This is exactly the situation Unity developers face with the IL2CPP scripting backend. IL2CPP converts C# to C++ and compiles to native code - the same principle as Native AOT. The result is a native binary that standard .NET decompilers cannot open. Yet IL2CPP games are routinely reverse-engineered using global-metadata.dat and tools like Il2CppDumper.
Native AOT and IL2CPP solve the same performance problem using the same approach. They offer the same partial protection as a side-effect. And they have the same fundamental limitation: native compilation is not hardening.
See also: Unity C# Obfuscation →
The compatibility constraints of Native AOT
Even setting security aside, Native AOT is not available to all .NET developers. It imposes significant constraints:
Reflection limitations. Native AOT's trimmer removes code that cannot be statically proven to be reachable. Any use of Type.GetType() by string, Activator.CreateInstance() with dynamic type names, or Assembly.Load() requires explicit preservation hints or breaks at runtime. Applications that use reflection extensively - many WPF, WinForms, and ORM-based applications - cannot adopt Native AOT without significant refactoring.
No runtime code generation. System.Reflection.Emit, System.Linq.Expressions with compilation, and dynamic proxies are not supported. Libraries that rely on these (some serializers, dependency injection containers, and ORMs) are incompatible or require alternative implementations.
Larger binary sizes. Native AOT binaries are self-contained and typically significantly larger than framework-dependent deployments. A simple .NET application that is a few hundred KB as a framework-dependent DLL may be 10-20MB as a Native AOT binary.
Longer build times. Native compilation is slower than managed compilation. For large applications, Native AOT build times can add minutes to CI/CD pipelines.
Platform-specific outputs. A Native AOT binary compiled on Windows produces a Windows executable. Building for multiple platforms requires multiple build environments or cross-compilation tooling. Android Native AOT support remains experimental as of .NET 10, with no built-in Java interop - which has direct implications for MAUI Android builds.
ReadyToRun is not a middle ground for security
Developers evaluating Native AOT sometimes consider ReadyToRun (R2R) as a compromise between standard JIT deployment and full Native AOT. From a security perspective this is an important distinction to get right.
ReadyToRun compiles your assemblies ahead of time but keeps both the native code and the original IL in the binary. At runtime, the JIT can still replace R2R methods with JIT-compiled code in tiered compilation. This means a ReadyToRun binary is readable by ILSpy exactly like a standard managed assembly - the IL is present, the metadata is present, nothing is removed.
ReadyToRun is a performance optimization. It provides no protection advantage compared to standard managed code. If anything, R2R binaries are larger than standard assemblies because they contain both representations. Do not use ReadyToRun as a security measure.
For many commercial ISV applications - particularly desktop applications with rich UIs, data binding, and ORM-based persistence - Native AOT is not a practical option today regardless of its security properties.
Can you combine Native AOT with ArmDot obfuscation?
The ideal approach would be to obfuscate the IL assembly first, then compile the obfuscated IL to native code with Native AOT. This would combine the analysis resistance of native compilation with the technique-level protection of obfuscation - string encryption, renamed symbols, and virtualized logic all compiled into a native binary.
Currently, ArmDot's control flow obfuscation and code virtualization use the calli CIL instruction for dispatch. Native AOT does not support calli. This means applying ArmDot's strongest protection techniques before Native AOT compilation breaks the build.
A fix is in development. The plan is to replace calli-based dispatch with virtual method calls, which achieve the same static analysis resistance - hiding the call target so a static analyzer cannot follow the dispatch - while being fully compatible with Native AOT. Virtual method dispatch is supported by Native AOT, and the protection property is equivalent: the call target is determined at runtime rather than being visible in the static code.
When this fix ships, the combined workflow will be: apply ArmDot (renaming, string encryption, control flow obfuscation, virtualization) → compile the obfuscated IL to native with Native AOT → distribute a native binary with no IL, no readable symbols, and no recoverable string literals. That will be a meaningfully stronger protection than either technique alone.
Until then: ArmDot's symbol renaming and string encryption do not use calli and can be applied before Native AOT compilation today. Control flow obfuscation and virtualization require the upcoming fix.
Who should use Native AOT, obfuscation, or both
The right answer depends on who your actual adversary is and what they are willing to do. This is worth thinking through explicitly, because the answer is not the same for every application.
A casual developer who downloads your app and opens it in a decompiler tool out of curiosity is deterred by Native AOT. They do not have Ghidra, they do not have native reverse engineering skills, and the IL-level attack path is closed. For an application whose threat model is opportunistic amateur analysis, Native AOT may be adequate protection for its cost.
A motivated competitor who wants to understand your proprietary algorithm, a determined cracker building a keygen for your license system, or a security researcher looking for vulnerabilities - these adversaries have Ghidra, have time, and have the skills to work with native binaries. For them, Native AOT raises the cost of analysis without eliminating the threat. This is where obfuscation adds protection that AOT cannot.
Native AOT without obfuscation is appropriate when your application is compatible with AOT's constraints, your primary goal is performance, and your threat model is casual inspection rather than motivated reverse engineering. The partial protection is a useful side-effect, not a security guarantee.
Obfuscation without Native AOT is appropriate for the large majority of commercial .NET applications - WPF, WinForms, MAUI, Blazor WASM, server-side tools - where AOT's compatibility constraints make it impractical. Obfuscation applies to the IL layer and does not require any architectural changes to the application.
Both, once the compatibility fix ships will be the strongest available option for applications that can adopt Native AOT. Obfuscated IL compiled to native code combines the barriers of both approaches. Tracking the ArmDot changelog for the Native AOT compatibility update is worthwhile if this is your target.
Related: Code Virtualization for .NET → - virtualization provides protection at the IL layer that survives into native compilation once the calli constraint is resolved.
Related: Unity C# Obfuscation → - IL2CPP presents the same tradeoffs as Native AOT. The conclusions transfer directly.
Back to: .NET Obfuscation vs Alternatives →
Protect your .NET application with ArmDot
ArmDot applies symbol renaming and string encryption before Native AOT compilation today. Full control flow obfuscation and virtualization compatibility with Native AOT is in development. For standard .NET deployments without AOT, the full protection stack is available now. Free trial available.
