COM Descriptor (.NET)¶
Prefer the interpreted view first
PPEE's .NET analysis reads this metadata for you and reports identity, references, P/Invoke, resources and structure warnings such as duplicate or fake streams automatically. This page describes the raw structures underneath.
Data directory 14
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR: DataDirectory[14] in the Optional header. All data directories
Despite its historical name, this directory marks a file as a .NET assembly: it points to the CLR (COR20) header, which in turn points to the ECMA-335 metadata (types, methods, fields, strings) and the IL code. A large share of commodity malware (stealers, RATs, loaders) is written in .NET, and the metadata layer is exactly where obfuscators and protectors leave fingerprints. PPEE parses it without the .NET runtime, on any platform. The status bar shows EXE(.Net) or DLL(.Net).
How the structure works¶
DataDirectory[14] ──► IMAGE_COR20_HEADER
├─ MajorRuntimeVersion.Minor (2.5 for all modern assemblies)
├─ Flags (ILONLY, 32BITREQUIRED, STRONGNAMESIGNED, NATIVE_ENTRYPOINT, …)
├─ EntryPointToken / RVA (0x06xxxxxx = a MethodDef; an RVA when NATIVE_ENTRYPOINT)
├─ Resources, StrongNameSignature, VTableFixups, ManagedNativeHeader
└─ MetaData ──► metadata root "BSJB"
├─ version string (v2.0.50727 = .NET 2–3.5, v4.0.30319 = .NET 4.x / Core)
└─ stream headers
├─ #~ (or #-) metadata tables (compressed / uncompressed)
├─ #Strings identifiers: type, method, field names
├─ #US user strings: every string literal in the code
├─ #GUID module GUIDs (MVID)
└─ #Blob signatures, constants, custom-attribute data
| COR20 flag | Value | Meaning |
|---|---|---|
ILONLY | 00000001 | Pure IL, with no native code |
32BITREQUIRED | 00000002 | Must run as a 32-bit process |
IL_LIBRARY | 00000004 | IL library |
STRONGNAMESIGNED | 00000008 | Has a strong-name signature |
NATIVE_ENTRYPOINT | 00000010 | The entry point is a native RVA, not a MethodDef token |
TRACKDEBUGDATA | 00010000 | Debug tracking |
32BITPREFERRED | 00020000 | "Prefer 32-bit" (AnyCPU 32-bit preferred) |
What PPEE shows¶
DIR_ENTRY_COM_DESCRIPTOR CLR header fields (flags decoded, entry point resolved)
└─ MetaData metadata root: signature, version string, stream directory
├─ #~ metadata tables: Module(1), TypeRef(139), TypeDef(172), Field, Method, Param, MemberRef,
│ CustomAttribute, ImplMap (P/Invoke), ManifestResource, … one row per record, fields in the lower list
├─ #Strings(n) Offset · Value
├─ #US(n) Offset · Value (string literals)
├─ #GUID(n) Index · Value
└─ #Blob(n) Index · first 16 bytes · Length
└─ VTableFixups RVA · Count · Type (decoded) · the method tokens
Tokens and heap indexes resolve to what they point at (Field[0x0001], Method[0x0001], type and namespace names), and every cell supports Follow in Hex View (Ctrl+H). Table and heap cells are editable, in the GUI and with --set.
Reading .NET assemblies like an analyst¶
| Observation | What it suggests |
|---|---|
Duplicate stream names (two #GUID, two #Strings, …) | An obfuscator: tools and the runtime may read different copies, so decompilers show fake metadata |
Look-alike stream names (#GUlD with a lowercase L, #Blop) | Deliberate confusion aimed at tools that match stream names loosely |
Junk streams (#<Module>, #null, #Schema, random names) | Padding streams added by protectors |
A stream named after the protector (#XerinFuscator) | The obfuscator identifies itself: pick the matching deobfuscator |
#- instead of #~ | Uncompressed/ENC metadata, a format rarely produced by normal compilers and used by obfuscators to break parsers |
Only mscoree.dll!_CorExeMain imported, entropy ≥ 7.5 | Encrypted payload in resources or #Blob: a crypter or loader that decrypts the real assembly in memory |
Flags without ILONLY, native imports (MSVCP140.dll, …), VTableFixups | Mixed-mode C++/CLI: native code hides beside the IL, so a .NET decompiler alone isn't enough |
NATIVE_ENTRYPOINT flag | Execution starts in native code |
| COR20 header present but no parsable metadata root | Corrupted or deliberately damaged. The file may not even run, or it relies on runtime patching |
#US strings with URLs, paths, cmd.exe, Base64 blobs | IOCs and configuration, often the fastest win in .NET triage |
ImplMap rows (P/Invoke) to kernel32!VirtualAlloc, CreateRemoteThread | Managed code calling native injection APIs |
CLI and JSON¶
$ ppee-cli --net managed.dll
.NET (COR20 header):
cb=48 RuntimeVersion=2.5 Flags=18 EntryPointToken=26314
MetaData: RVA=8DA0 Size=13504
Metadata root: Signature=424A5342 Version=1.1 VersionString="v2.0.50727" Flags=0 Streams=5
#~ offset=6C size=66F4
#Strings offset=6760 size=94E0
JSON: net → present, header (cb, runtime version, flags, entryPointToken, metaDataRva/Size, resourcesRva/Size, strongNameSignatureRva/Size, vTableFixupsRva, …), metadataRoot (signature, versionString, numberOfStreams, streams[] of name/offset/size), vTableFixups[]. The table rows and heap contents are shown in the GUI.
Hunting recipes¶
# Is it .NET, and which runtime?
ppee-cli --json --net f.exe | jq -r 'if .net.present then "\(.net.metadataRoot.versionString) flags=\(.net.header.flags)" else "native" end'
# Obfuscation indicators in the stream directory
ppee-cli --json --net f.exe | jq '[.net.metadataRoot.streams[].name] as $s
| {streams: $s,
duplicates: ($s | group_by(.) | map(select(length > 1) | .[0])),
nonStandard: ($s - ["#~", "#-", "#Strings", "#US", "#GUID", "#Blob"]),
uncompressedTables: ($s | index("#-") != null)}'
# Triage a folder: runtime, flags, entropy, suspicious streams
for f in samples/*; do
ppee-cli --no-similarity --json --net --hashes "$f" 2>/dev/null | jq -r --arg f "$f" 'select(.net.present)
| [.net.metadataRoot.streams[].name] as $s
| [$f, .net.metadataRoot.versionString, .net.header.flags, (.fileInfo.entropy * 100 | floor / 100),
(($s - ["#~", "#-", "#Strings", "#US", "#GUID", "#Blob"]) | join(","))] | @tsv'
done
# Mixed-mode? (not ILONLY, or native imports besides mscoree)
ppee-cli --json --net --imports f.exe | jq '(.net.header.flags | ascii_downcase | explode | reduce .[] as $c (0; . * 16 + (if $c >= 97 then $c - 87 else $c - 48 end))) as $fl
| {ilOnly: ($fl % 2 == 1), nativeImports: [.imports[].name | select(ascii_downcase != "mscoree.dll")]}'
Related: --net · JSON net · Editing metadata tables · Strings
References¶
- ECMA-335: Common Language Infrastructure: the standard that defines .NET metadata tables, heaps and IL.
