Introduction to .NET & the CLR

25 minIn Progress

Introduction to .NET & the CLR

Why .NET Malware Deserves Special Attention

.NET malware has become one of the most prevalent categories in the threat landscape. Families like AsyncRAT, Agent Tesla, RedLine Stealer, and Quasar RAT are all built on the .NET Framework. Understanding why .NET is different from native code is the foundation for every technique you will learn in this module.

Unlike native Windows executables that compile directly to x86/x64 machine instructions, .NET programs compile to Common Intermediate Language (CIL) -- a platform-independent bytecode that the Common Language Runtime (CLR) translates to native code at execution time via Just-In-Time (JIT) compilation. This architecture has profound implications for malware analysis:

  • Near-perfect decompilation -- CIL bytecode retains class names, method names, variable names, and even string literals. Tools like dnSpyEx and ILSpy can reconstruct code that reads almost identically to the original C# source.
  • No disassembly guesswork -- You do not need to interpret raw x86 instructions or reconstruct function prototypes. The metadata system does that for you.
  • Rich metadata tables -- Every .NET assembly carries type definitions, method signatures, a string heap, and a resource manifest. This metadata is a goldmine for analysts.
  • Reflective loading capability -- .NET supports Assembly.Load(byte[]), allowing malware to load entire executables from memory without ever touching disk. This is a core technique in fileless .NET malware chains.

Key Insight: Because .NET decompilation is so effective, malware authors invest heavily in obfuscation. The arms race between decompilers and obfuscators defines much of .NET malware analysis.


How .NET Execution Works Under the Hood

When Windows loads a .NET executable, the process is fundamentally different from a native PE:

1. Windows PE loader reads the PE header
2. The import table references mscoree.dll -> _CorExeMain
3. _CorExeMain initializes the CLR runtime
4. CLR reads the metadata tables and locates the entry point token
5. CLR JIT-compiles CIL bytecode to native code on-the-fly
6. Execution begins at the Main() method (or module initializer)

This means that the .text section of a .NET PE does not contain native machine code -- it contains CIL bytecode. Disassemblers like Ghidra or IDA will show mostly garbage if you try to treat it as native code. You need .NET-specific tools.

.NET Framework vs .NET (Core) vs Mono

RuntimePlatformsMalware Prevalence
.NET Framework 4.xWindows onlyVery high -- most .NET malware targets this
.NET 6/7/8 (Core)Cross-platformGrowing -- enables Linux/macOS targeting
MonoCross-platformRare -- occasionally in game-related malware

Most .NET malware you will encounter targets .NET Framework 4.x because it is pre-installed on modern Windows systems, providing a guaranteed execution environment.


Identifying .NET Executables

Correctly identifying a binary as .NET is the first step -- it determines your entire tool chain. There are multiple ways to verify:

Method 1: PEStudio Quick Triage

Open the sample in PEStudio and check:

  • Imports: Look for mscoree.dll with the import _CorExeMain (EXE) or _CorDllMain (DLL)
  • Sections: The .text section will be flagged as containing managed code
  • Version Info: May reference .NET Framework or specific CLR versions

Method 2: Detect It Easy (DiE)

DiE signature matching reports the compiler:

Compiler: Microsoft Visual C# / Basic .NET
Library:  .NET Framework v4.0.30319
Linker:   Microsoft Linker(48.0)

Method 3: Programmatic Detection with pefile

import pefile

pe = pefile.PE("suspect.exe")

# Check the COM Descriptor directory (index 14)
# This is the CLR header -- present only in .NET assemblies
clr_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[14]
if clr_dir.VirtualAddress != 0:
    print("[+] .NET executable detected")
    print(f"    CLR Header RVA: 0x{clr_dir.VirtualAddress:08x}")
    print(f"    CLR Header Size: {clr_dir.Size} bytes")
else:
    print("[-] Not a .NET executable")

# Cross-check: look for mscoree.dll import
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        if b"mscoree" in entry.dll.lower():
            print("[+] Confirmed: imports mscoree.dll")

Method 4: Command-Line with file / CAPA

# Linux/macOS file command
file suspect.exe
# Output: PE32 executable (GUI) Intel 80386 Mono/.Net assembly

# CAPA capability detection
capa suspect.exe | grep -i ".net"

Analyst Tip: If you accidentally open a .NET binary in Ghidra or IDA, you will see the _CorExeMain import in the entry point thunk. This is your signal to switch to dnSpyEx or ILSpy.


.NET Assembly Internal Structure

Every .NET assembly is a standard PE file with additional .NET-specific structures layered on top:

ComponentPurposeAnalyst Value
PE HeaderStandard Windows executable wrapperEntry point, subsystem, timestamp
CLR HeaderPoints to metadata root, entry point token, flagsDetermines .NET version, strong naming
Metadata TablesTypeDef, MethodDef, FieldDef, MemberRef, StringHeapClass/method names, string constants
CIL Method BodiesIntermediate language instructions for each methodThe actual program logic
ResourcesEmbedded files, icons, satellite assemblies, config blobsOften contains encrypted payloads or configs
Strong Name SignatureOptional RSA signature for assembly integrityRarely used by malware

Key Metadata Tables for Analysts

TableWhat It ContainsWhy It Matters
TypeDefAll classes/structs defined in the assemblyReveals program architecture
MethodDefAll methods with their CIL bodiesContains the executable logic
MemberRefReferences to external types/methodsShows API usage and dependencies
StandAloneSigLocal variable signaturesReveals function complexity
CustomAttributeAttributes applied to types/methodsMay reveal obfuscator artifacts
ManifestResourceEmbedded resourcesOften hides encrypted payloads

Key .NET Malware Families

Understanding the landscape of .NET malware helps you know what to expect during analysis:

FamilyTypeNotable Characteristics
AsyncRATRemote Access TrojanOpen-source, Settings class with plaintext/encrypted config
Agent TeslaInfostealer/KeyloggerHeavy obfuscation, encrypted config in resources, exfil via SMTP/FTP
RedLine StealerCredential StealerTargets browser data, crypto wallets, system info
Quasar RATRemote Access TrojanOpen-source, plugin architecture, certificate pinning
NanoCoreRemote Access TrojanPlugin-based, config in encrypted resources
njRATRemote Access TrojanLightweight, VB.NET, plaintext C2 in source
Formbook/XLoaderInfostealer.NET loader wrapping native payload
RemcosRemote Access Trojan.NET wrapper around native core

Analyst Tip: Many of these families are open-source (AsyncRAT, Quasar). Reading their source on GitHub gives you a head start -- you will recognize the code patterns when you encounter them in the wild.


Reflective Loading: Assembly.Load()

One of the most important .NET capabilities for malware is reflective code loading via Assembly.Load(byte[]). This allows a .NET program to load and execute an entire assembly from a byte array in memory -- no file needs to touch disk.

Typical multi-stage .NET malware chain:

Stage 1: Loader (on disk)
   |
   +-> Decrypts embedded resource to byte[]
   +-> Assembly.Load(decryptedBytes)  // Stage 2 loaded in memory
       |
       +-> Stage 2 decrypts another payload
       +-> Assembly.Load(nextStage)   // Stage 3 in memory
           |
           +-> Final payload (RAT, stealer, etc.)

This pattern is extremely common. The chatroom.exe sample from SANS FOR610 demonstrates exactly this: a three-stage chain where Stage 1 loads "Windows Mddules.dll" via Assembly.Load(), which in turn extracts and loads the final ReZer0V4.exe payload.

Detection Points for Reflective Loading

IndicatorWhere to Look
Assembly.Load or Assembly.LoadFrom callsDecompiled source in dnSpyEx
Large embedded byte arrays in resourcesResource viewer in dnSpyEx/ILSpy
Convert.FromBase64String usageDecompiled source
ETW CLR Assembly Load eventsRuntime monitoring / ETW traces

The .NET Malware Analysis Workflow

Here is the systematic approach you should follow for every .NET sample:

  1. Identify as .NET -- PEStudio imports check for mscoree.dll, DiE compiler detection
  2. Check for obfuscation -- Run DiE, check for garbled names in decompiler, try de4dot
  3. De-obfuscate if needed -- Run de4dot suspect.exe to produce a cleaned version
  4. Decompile -- Open in dnSpyEx (primary) or ILSpy (secondary)
  5. Find the entry point -- Locate the Main() method or Module initializer (.cctor)
  6. Trace execution flow -- Follow method calls from the entry point forward
  7. Identify reflective loading -- Look for Assembly.Load patterns and embedded resources
  8. Extract configuration -- Find C2 addresses, encryption keys, mutex names, install paths
  9. Map to MITRE ATT&CK -- Document techniques observed (T1620, T1059.001, etc.)
  10. Extract IOCs -- URLs, IP addresses, file hashes, registry paths, mutexes

Common Pitfall: Do not skip the obfuscation check. Opening a heavily obfuscated .NET binary in dnSpyEx without running de4dot first wastes significant time. Always try de4dot first -- even if it fails, it will often tell you which obfuscator was used.


MITRE ATT&CK Mapping

BehaviorTechnique IDTechnique Name
.NET assembly executionT1059Command and Scripting Interpreter
Reflective code loadingT1620Reflective Code Loading
Obfuscated assembliesT1027.002Software Packing
Embedded payloads in resourcesT1027.009Embedded Payloads
Dynamic API resolutionT1027.007Obfuscated Files or Information: Dynamic API Resolution
Try it in the shell
Practise this lesson's tooling on its sample in an emulated analyst shell. Output is pre-recorded — nothing executes.

Suggested triage steps

  1. 1

    Spot the managed assembly

    A .NET binary announces itself in the file type — 'Mono/.Net assembly' is what separates it from a native PE.

  2. 2

    Find the CLR loader stub

    Every .NET assembly imports exactly one function, _CorExeMain from mscoree.dll. A near-empty import table IS the tell.

  3. 3

    Confirm the runtime dependency

    mscoree.dll is the CLR itself. A binary that needs it is managed code, which is why a decompiler recovers near-source C# rather than assembly.

analyst@lab:~emulated · nothing executes

MAA analyst shell — emulated. Nothing executes.

Type 'help', or click a step on the left.

$
Introduction to .NET & the CLR | Malware Analysis Academy