Native AOT
Yes, you can obfuscate a Native AOT app, but only if the obfuscation happens on the managed IL before
the AOT compiler runs. BitMono is an IL obfuscator, so it has to do its work before ilc (the
Native AOT compiler) turns your assembly into native code.
Warning
Native AOT plus IL obfuscation is not officially supported by Microsoft. The .NET team has said
there are no supported hooks for third-party code to modify the inputs to ilc
(dotnet/runtime#121522). It works in practice,
but the exact MSBuild wiring can change between SDK versions (a known symptom on some .NET 9 SDKs is
error: No entrypoint module during ilc). Treat it as a community integration: pin your SDK
and test the published native binary.
What changes with AOT
A normal dotnet publish leaves a managed .dll in the output that BitMono rewrites directly.
With PublishAot, dotnet publish instead:
compiles your C# to a managed IL assembly in
obj/<config>/<tfm>/<rid>/(the intermediate assembly), thenruns
ilcto turn that IL and its dependencies into one native executable.
There’s no managed assembly left in the output to touch. So BitMono has to run between those two steps,
on the intermediate assembly, before ilc reads it.
Keep in mind the final binary is native code, so renaming buys you less than usual (there’s no IL left
for a decompiler), and the protections that rely on IL/metadata or runtime tricks (AntiDecompiler,
BitMono, DotNetHook, CallToCalli, …) are pointless or even harmful under AOT. For AOT
stick to lightweight IL-level transforms like StringsEncryption and FullRenamer, and if you want
native-level protection put a native protector on top.
The easy way: the MSBuild NuGet integration
BitMono.Integration obfuscates the intermediate assembly right after
compilation (AfterCompile), which is before ilc consumes it. So for most projects AOT needs no
extra wiring, just add the package and publish:
dotnet publish -c Release -r win-x64 /p:PublishAot=true
If the app starts and runs, you’re done. If ilc fails with No entrypoint module (or a similar
input error), you hit the unsupported-hook limitation above, try pinning to an SDK where it’s known to
work for you (it has historically worked on .NET 8).
Doing it by hand
Not using the NuGet package? Hook a target that runs before IlcCompile and overwrite the
intermediate assembly in place. Don’t move or rename it, keeping the same path is what stops the
ilc input list from desyncing:
<Target Name="BitMonoBeforeAot"
BeforeTargets="IlcCompile"
Condition="'$(PublishAot)' == 'true'">
<Exec Command="BitMono.CLI -f "$(IntermediateOutputPath)$(TargetName).dll" -l "$(IntermediateOutputPath)" -o "$(IntermediateOutputPath)bitmono" -n "$(TargetName).dll"" />
<Copy SourceFiles="$(IntermediateOutputPath)bitmono\$(TargetName).dll"
DestinationFiles="$(IntermediateOutputPath)$(TargetName).dll"
OverwriteReadOnlyFiles="true" />
</Target>
Things to watch out for
Keep renaming AOT-safe. Native AOT leans on metadata for some reflection and its own bookkeeping, so exclude anything read by reflection with
criticals.json/[Obfuscation(Exclude = true)]and keepReflectionMembersObfuscationExcludeon.Members reached through a generic instantiation (e.g.
Foo<int>().Bar()) used to get renamed without their references being updated, which crashed AOT and JIT apps alike. That’s fixed now, FullRenamer rewrites those references, just use a current BitMono build.No entrypoint moduleonilcis the unsupported-hook limitation (dotnet/runtime#121522), not a bug in your config. It depends on the SDK version.
.NET MAUI
Android works with the normal flow, the code stays managed IL, so add the BitMono.Integration package (or run the CLI) and build as usual.
iOS is AOT-compiled (no JIT allowed), so the app head becomes a native arm64 image BitMono can’t read, you’ll see
unsupported PE image architecture Arm64. To protect an iOS app, obfuscate the IL before AOT: move the code you care about into class libraries and obfuscate those. They stay managed IL when BitMono rewrites them and get AOT-compiled afterwards, so the protection carries into the native output. Don’t try to obfuscate the iOS app head itself.