Skip to content

Export Directory

Data directory 0

IMAGE_DIRECTORY_ENTRY_EXPORT: DataDirectory[0] in the Optional header. All data directories

The export directory lists what a module offers to others: function names, ordinals and addresses. For malware analysis it answers "how is this DLL meant to be started?" and "is this DLL pretending to be another one?"

How the structure works

IMAGE_EXPORT_DIRECTORY points to three parallel arrays:

Table Content
Export Address Table (EAT) One RVA per ordinal. An RVA that points inside the export directory is a forwarder string such as kernel32.AcquireSRWLockShared
Name Pointer Table RVAs of the exported names, sorted alphabetically for binary search
Ordinal Table For each name, its index into the EAT
Header field Meaning
Name The DLL's internal name as recorded at link time. It doesn't have to match the file name, which is useful evidence
Base The first ordinal number (usually 1)
NumberOfFunctions / NumberOfNames Size of the EAT and number of named exports. Functions without names are exported by ordinal only
TimeDateStamp Often mirrors the link time, or 0

What PPEE shows

  • Upper list, one row per export: Ordinal · RVA - Section name (the section the code lives in) · Name RVA · Name · Demangled name · Forwarded to.
  • Header fields (name, base, counts, timestamp) are shown with the directory.
  • An export RVA that lands outside any executable section, or a forwarder to an unusual module, stands out immediately.
  • Every cell supports Follow in Hex View (Ctrl+H), and names and RVAs are editable.

Windows screenshot: Exports of a ransomware DLL: an internal name that differs from the file name, and six exports that are all data in .data

Exports of a ransomware DLL: an internal name that differs from the file name, and six exports that are all data in .data

A ransomware DLL (ef431e36….dll) shows two things at a glance:

  • The internal Dll Name, affiliate_21sys_4a1f3a9c.dll, isn't the file name, so it's in the warning color. The name reads like a per-affiliate build ID.
  • All six "exports" point into .data [RW], not code. g_data (at the start of .data, followed by 3.7 MB) is the embedded payload, g_len its length, and g_k1/g_k2/g_ko1/g_ko2 look like key material. A DLL that exports data like this is a container for a payload, not a library.

Reading exports like an analyst

Implant and loader entry points

A malicious DLL has to be started somehow, and its exports show how:

Export How it is typically started
DllRegisterServer / DllUnregisterServer / DllInstall regsvr32 /s evil.dll or regsvr32 /i /n, a classic LOLBin execution path
ServiceMain Loaded by svchost.exe as a service DLL
DllGetClassObject / DllCanUnloadNow COM hijacking (CLSID InprocServer32)
Any name used with rundll32 evil.dll,Entry Direct execution
_cgo_dummy_export The DLL was built with Go (cgo), a common implant language

Real sample: a Go-based C2 implant DLL

$ ppee-cli --json --exports merlin.x86.dll | jq -r '.exports.name, ([.exports.functions[].name] | join(", "))'
merlin.x86.dll
DllInstall, DllRegisterServer, DllUnregisterServer, Merlin, Run, VoidFunc, _cgo_dummy_export
The Merlin C2 agent exposes several start-up paths (regsvr32, rundll32 …,Run), and _cgo_dummy_export gives away the Go toolchain.

Masquerading and sideloading

Attackers often plant a DLL named after one that a signed, trusted program loads, which is DLL sideloading. The export table usually gives it away:

Real sample: a fake Windows Defender client library

$ ppee-cli --json --exports malware_with_sections.dll | jq -r '.exports.name, ([.exports.functions[].name] | join(","))'
file.exe
MpAllocMemory,MpClientUtilExportFunctions,MpConfigClose,MpConfigGetValue,…,MpFreeMemory,_cgo_dummy_export
It exports the Mp* API of Defender's mpclient.dll so a Defender binary will load it. But the internal export name is file.exe, and _cgo_dummy_export shows it is a Go build, which Microsoft's real DLL isn't.

Things to compare against the genuine DLL:

  • Internal name vs file name (file.exe for a .dll is a red flag).
  • Export count: stubs usually export only what the host program calls.
  • Which exports have real code vs stubs that just return.
  • Timestamp and signature (see Security): genuine system DLLs are Microsoft-signed.

Proxy DLLs (forwarding)

A proxy DLL forwards most exports to the real library and implements only the few it wants to intercept:

Real sample: a kernel32 wrapper

$ ppee-cli --json --exports k32wrap.dll | jq '{total: .exports.numberOfFunctions,
    forwarded: ([.exports.functions[] | select(.forwarder)] | length)}'
{"total": 1302, "forwarded": 1270}
$ ppee-cli --json --exports k32wrap.dll | jq -r '.exports.functions[] | select(.forwarder | not) | .name' | head -4
ClosePseudoConsole
CreateFile2
CreateFileMappingW
CreateProcessW
1270 exports go straight to kernel32. The 32 implemented ones (CreateProcessW, CreateFileMappingW, …) are exactly the functions it hooks, and they're the ones to reverse.

CLI and JSON

$ ppee-cli --exports aepic.dll
Exports: AEPIC.dll (base=1, 12 function(s)):
  #1      GetAppInventoryCore rva=29CB0
  #2      UpdateSoftwareInventoryTC2 rva=4A3F0

JSON: exports → present, name, characteristics, timeDateStamp, base, numberOfFunctions, numberOfNames, functions[] (ordinal, name, and either rva or forwarder).

Hunting recipes

# Internal name different from the file name?
f=sample.dll; ppee-cli --json --exports "$f" | jq -r --arg fn "$(basename "$f")" \
  'select(.exports.present and (.exports.name | ascii_downcase) != ($fn | ascii_downcase)) | "name mismatch: \(.exports.name)"'

# Execution-related exports across a folder
for f in samples/*.dll; do
  ppee-cli --no-similarity --json --exports "$f" 2>/dev/null | jq -r --arg f "$f" '[.exports.functions[].name // empty]
    | map(select(test("^(DllRegisterServer|DllInstall|ServiceMain|DllGetClassObject)$"))) | select(length > 0) | "\($f): \(join(","))"'
done

# Proxy DLL ratio
ppee-cli --json --exports f.dll | jq '(.exports.functions | length) as $n | ([.exports.functions[] | select(.forwarder)] | length) as $fw
  | {exports: $n, forwarded: $fw, proxyLike: ($n > 0 and $fw / $n > 0.8)}'

# Ordinal-only exports (no name)
ppee-cli --json --exports f.dll | jq -r '.exports.functions[] | select(.name == null) | "#\(.ordinal) \(.rva)"'

MCP: list_exports · Related: --exports · Import directory · Security (verify the signer)

References