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
| Runtime | Platforms | Malware Prevalence |
|---|---|---|
| .NET Framework 4.x | Windows only | Very high -- most .NET malware targets this |
| .NET 6/7/8 (Core) | Cross-platform | Growing -- enables Linux/macOS targeting |
| Mono | Cross-platform | Rare -- 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.dllwith the import_CorExeMain(EXE) or_CorDllMain(DLL) - Sections: The
.textsection will be flagged as containing managed code - Version Info: May reference
.NET Frameworkor 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
_CorExeMainimport 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:
| Component | Purpose | Analyst Value |
|---|---|---|
| PE Header | Standard Windows executable wrapper | Entry point, subsystem, timestamp |
| CLR Header | Points to metadata root, entry point token, flags | Determines .NET version, strong naming |
| Metadata Tables | TypeDef, MethodDef, FieldDef, MemberRef, StringHeap | Class/method names, string constants |
| CIL Method Bodies | Intermediate language instructions for each method | The actual program logic |
| Resources | Embedded files, icons, satellite assemblies, config blobs | Often contains encrypted payloads or configs |
| Strong Name Signature | Optional RSA signature for assembly integrity | Rarely used by malware |
Key Metadata Tables for Analysts
| Table | What It Contains | Why It Matters |
|---|---|---|
TypeDef | All classes/structs defined in the assembly | Reveals program architecture |
MethodDef | All methods with their CIL bodies | Contains the executable logic |
MemberRef | References to external types/methods | Shows API usage and dependencies |
StandAloneSig | Local variable signatures | Reveals function complexity |
CustomAttribute | Attributes applied to types/methods | May reveal obfuscator artifacts |
ManifestResource | Embedded resources | Often hides encrypted payloads |
Key .NET Malware Families
Understanding the landscape of .NET malware helps you know what to expect during analysis:
| Family | Type | Notable Characteristics |
|---|---|---|
| AsyncRAT | Remote Access Trojan | Open-source, Settings class with plaintext/encrypted config |
| Agent Tesla | Infostealer/Keylogger | Heavy obfuscation, encrypted config in resources, exfil via SMTP/FTP |
| RedLine Stealer | Credential Stealer | Targets browser data, crypto wallets, system info |
| Quasar RAT | Remote Access Trojan | Open-source, plugin architecture, certificate pinning |
| NanoCore | Remote Access Trojan | Plugin-based, config in encrypted resources |
| njRAT | Remote Access Trojan | Lightweight, VB.NET, plaintext C2 in source |
| Formbook/XLoader | Infostealer | .NET loader wrapping native payload |
| Remcos | Remote 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
| Indicator | Where to Look |
|---|---|
Assembly.Load or Assembly.LoadFrom calls | Decompiled source in dnSpyEx |
| Large embedded byte arrays in resources | Resource viewer in dnSpyEx/ILSpy |
Convert.FromBase64String usage | Decompiled source |
| ETW CLR Assembly Load events | Runtime monitoring / ETW traces |
The .NET Malware Analysis Workflow
Here is the systematic approach you should follow for every .NET sample:
- Identify as .NET -- PEStudio imports check for mscoree.dll, DiE compiler detection
- Check for obfuscation -- Run DiE, check for garbled names in decompiler, try de4dot
- De-obfuscate if needed -- Run
de4dot suspect.exeto produce a cleaned version - Decompile -- Open in dnSpyEx (primary) or ILSpy (secondary)
- Find the entry point -- Locate the Main() method or Module initializer (.cctor)
- Trace execution flow -- Follow method calls from the entry point forward
- Identify reflective loading -- Look for Assembly.Load patterns and embedded resources
- Extract configuration -- Find C2 addresses, encryption keys, mutex names, install paths
- Map to MITRE ATT&CK -- Document techniques observed (T1620, T1059.001, etc.)
- 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
| Behavior | Technique ID | Technique Name |
|---|---|---|
| .NET assembly execution | T1059 | Command and Scripting Interpreter |
| Reflective code loading | T1620 | Reflective Code Loading |
| Obfuscated assemblies | T1027.002 | Software Packing |
| Embedded payloads in resources | T1027.009 | Embedded Payloads |
| Dynamic API resolution | T1027.007 | Obfuscated Files or Information: Dynamic API Resolution |
