Skip to content

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

Linux screenshot: .NET metadata TypeDef table

.NET metadata TypeDef table

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