Skip to content

.NET NativeAOT

NativeAOT compiles C# ahead of time to a native binary. There is no CLR header and no IL, so a .NET decompiler has nothing to open and the COM descriptor directory is empty. Attackers use it to get C# development speed with native-code opsec.

How PPEE detects NativeAOT

PPEE looks for a .managed section and the NativeAOT runtime's markers, and then for the module header (ReadyToRun-style header) that indexes the compiled code. The Detected line says which evidence it found, and whether the module header was located, for example a .managed section and the NativeAOT runtime's markers (no module header).

Views

View Shown when Contents
Summary always Runtime, application (exports), code, layout clues
Assemblies the module header is found Assemblies and their metadata
Methods the module header is found Methods recovered from the reflection metadata and the stack-trace map

Method rows link to their code: right-click → Show Code (or the corner mark) opens the Code window there.

What the Summary tells you

Section Facts
Runtime Whether a module header exists and which runtime generation it implies. The runtime build is guessed from version strings (labelled (guessed))
Application The exports (NativeAOT DLLs expose functions with [UnmanagedCallersOnly])
Code How much metadata and method naming could be recovered
Layout clues Overlay, sections with no file data, writable + executable sections

Walkthrough: an obfuscated NativeAOT DLL

$ ppee-cli --analysis g4tj2aybt7y2xoq92p5e4y.dll
NativeAOT
  Detected: a .managed section and the NativeAOT runtime's markers (no module header)
Runtime
  Runtime: no module header: .NET 7 or earlier, or the header was removed
  Runtime build (guessed): 7.0.20+0fb6ac59f [offset 0x17008B]
Application
  Exports: 0Hd7vVgfx9 [export row 0]
  : 1RpQ9jnj [export row 1]
  : CLRCreateInstance [export row 19]  …
Code
  Without a module header the metadata and method names can't be located.
Layout clues
  - Overlay (appended data): 6144.0 KB [offset 0x3A9600]
Three observations from one command: the runtime is .NET 7 (a build guess from an embedded version string), there are 229 exports, most with random names, next to lookalikes of real ones like CLRCreateInstance, CloseCtrs and CoEEShutDownCOM (decoys or a generated export table), and a 6 MB overlay, likely the payload.

With a module header: a ransomware names itself

When the module header is still there, PPEE recovers the assemblies and the method names, and a NativeAOT binary tells you much more than its native code alone.

Windows screenshot: NativeAOT Summary of a ransomware: .NET 10 module header 16.0, entry assembly NoMatter, 15 assemblies and 4259 named methods

NativeAOT Summary of a ransomware: .NET 10 module header 16.0, entry assembly NoMatter, 15 assemblies and 4259 named methods

d1337711….exe (2.2 MB, x64): runtime .NET 10 (module header 16.0), entry assembly NoMatter 1.0.0.0, 15 assemblies and 4,259 named methods. The assembly list is a capability list already: System.Security.Cryptography, System.IO.FileSystem.DriveInfo (walk every drive), System.Diagnostics.Process (run commands), Microsoft.Win32.Registry.

Windows screenshot: NativeAOT Methods view: the NoMatter.Program methods DeleteShadowCopiesAsync, CollectFilesOptimized, EncryptSmallFileGCM, EncryptLargeFileCBC

NativeAOT Methods view: the NoMatter.Program methods DeleteShadowCopiesAsync, CollectFilesOptimized, EncryptSmallFileGCM, EncryptLargeFileCBC

Methods, with the program's own assembly first: DeleteShadowCopiesAsync, RunEncryptionAsync, CollectFilesOptimized, EncryptSmallFileGCM, EncryptLargeFileCBC (AES-GCM for small files, CBC for large ones) and NtSetInformationProcess. That is ransomware, named by its author. Right-click a row → Show Code to read the method in the Code window. The same file's manifest asks for requireAdministrator.

Reading a NativeAOT binary like an analyst

Observation What it suggests
.managed section, no CLR header, a normal .text NativeAOT, not native C++. Don't expect IL
No module header on a recent runtime The header was removed on purpose (or the runtime is .NET 7 or earlier, which lacks it): metadata and method names can't be recovered, so rely on strings and behavior
Randomly named exports in bulk A generated export table (decoys), or a builder that renames exports per sample. Compare the export count and any real names across samples
Large overlay Embedded payload or configuration. Run the deep pass to see its entropy
Known .NET API names among exports (CLRCreateInstance) Deliberate lookalikes of well-known CLR hosting functions
Runtime build string Ties samples to the same SDK/runtime version: a clustering key

CLI and JSON

ppee-cli --analysis f.dll
ppee-cli --json --analysis-deep f.dll | jq '.analysis.runtimes[] | select(.key == "aot")'

# How many exports, and the first few (a huge, random-looking table is itself a finding)
ppee-cli --json --exports f.dll | jq '{count: .exports.numberOfFunctions, sample: [.exports.functions[:5][] | .name]}'

Related: Analysis overview · .NET analysis · Export directory · --analysis

References