Skip to content

Debug Directory

Data directory 6

IMAGE_DIRECTORY_ENTRY_DEBUG: DataDirectory[6] in the Optional header. All data directories

The debug directory is an array of IMAGE_DEBUG_DIRECTORY entries left by the compiler and linker. The most valuable one, CodeView (RSDS), records the path of the PDB file on the build machine, together with a GUID and age that identify the exact build. For threat intelligence it's one of the best attribution and clustering artifacts in a PE file, because authors often forget it's there.

How the structure works

Entry field Meaning
Type What the entry describes (table below)
TimeDateStamp Usually the link time. For repro builds it's a hash, not a date
SizeOfData Size of the entry's data
AddressOfRawData / PointerToRawData RVA and file offset of the data

A CodeView RSDS record contains "RSDS", a 16-byte GUID, a 32-bit age and the PDB path. Symbol servers index PDBs by GUID + age, so these three values uniquely identify the build.

Type Name in PPEE Why you care
1 COFF Legacy COFF symbols
2 CODEVIEW PDB path, GUID, age
3 FPO Frame-pointer omission records (x86)
4 MISC Legacy (often a DBG file name)
9 BORLAND Borland/Embarcadero toolchain
12 VC_FEAT Counts of /GS, /sdl and similar compiler features
13 POGO Profile-guided optimization section records
14 ILTCG Built with link-time code generation
16 REPRO Deterministic build: the timestamps are hashes
17 EMDEDDED_PORTABLE_PDB A .NET portable PDB embedded in the file
19 PDB_CHECKSUM Hash of the matching PDB
20 EX_DLLCHARACTERISTICS Extended flags such as CET shadow-stack compatibility
21 PERFMAP .NET perf map

What PPEE shows

  • Upper list: one row per entry: Characteristics · TimeDateStamp · Meaning (the decoded date) · Major/MinorVersion · Type · SizeOfData · AddressOfRawData · PointerToRawData.
  • Child nodes decode each entry: CodeView (signature, GUID, age, PDB path), FPO (n) records, POGO (n) section records, and the others by type name.
  • Follow in Hex View (Ctrl+H) jumps to the raw entry data.

Windows screenshot: Debug directory of a shellcode loader: four entries and the decoded CodeView record with a PDB path under C:\Users\Administrator

Debug directory of a shellcode loader: four entries and the decoded CodeView record with a PDB path under C:\Users\Administrator

A shellcode loader (86c6bd80….exe): four entries above, and the CodeView record below with RSDS, the GUID, age 1 and the PDB path C:\Users\Administrator\source\repos\actami\x64\Release\actami.pdb. That is Visual Studio's default project folder, so the project name actami is the lead to search for. The Meaning column dates every entry (25 Jul 2026).

Reproducible builds are called out

When a REPRO entry is present, PPEE's runtime analysis says so in its Summary and links to the entry (and to the hash bytes when the entry carries them). In the GUI, the entry's Size row can be followed in the hex view.

Reading debug data like an analyst

Observation What it suggests
PDB path with a user name or project name (C:\Users\<name>\source\repos\stealer\Release\x.pdb) Attribution and clustering: search the path, or just the file name, across your sample set and in public sandboxes
PDB path from a build server (C:\TfsBuildTemp\…, D:\a\_work\1\s\…) Legitimate CI-built software, or a stolen/rebuilt copy of it
Same GUID + age in two files The same build, even if other bytes differ
No debug directory at all Stripped by the toolchain (Go, some Rust builds, many packers) or deliberately removed. Common in malware
Garbage entry types or sizes The directory size covers more than the real entries: parser confusion, or a tampered header
CodeView present but PDB path is empty or random Deliberately scrubbed or forged
REPRO entry Timestamps are hashes: don't use them to date the build

Real samples

  • STEALERDLL.dll names itself: D:\Mktmp\StealerDLL\Release.x64\STEALERDLL.pdb, age 212 (the author rebuilt it about 200 times).
  • RustyStealer builds keep short PDB names that still say what they are: injector.pdb, svc_agent.pdb, kuinabot.pdb, netcfg.pdb. Several unrelated-looking samples share newwapcu.pdb: one project, many builds.
  • Three malware samples (a Go C2 implant, a sideloading DLL and a stealer) have no debug directory.
  • A commercial binary leaks its internal build layout: C:\Package_QB_Agent\workspace\root\PACKAGES_IOS\DeXonPC_Windows\Schedule\Prod_OneUI40\src\Bin\Market\Win32\PDBFiles\SCommon.pdb.
  • A .NET library has a CodeView entry plus type 16 (REPRO) with TimeDateStamp 0: a deterministic build.
  • A component library's debug directory claims 4 entries, but only the first is real. The other three are the bytes of the PDB path read as entries; pointerToRawData=545C3A43 is ASCII C:\T:
    type=2 sizeOfData=107 … [RSDS pdb=C:\TfsBuildTemp\WinForms\Drop-ALL_452\Binaries\Release\C1.Win.C1FlexGrid.4.5.2.pdb …]
    type=2091898510 sizeOfData=3615382220 pointerToRawData=545C3A43
    type=1181641047 sizeOfData=1936552559 pointerToRawData=4C412D70
    

CLI and JSON

$ ppee-cli --debug STEALERDLL.dll
Debug directory: 4 entrie(s)
  type=2 sizeOfData=71 pointerToRawData=119EDC [RSDS pdb=D:\Mktmp\StealerDLL\Release.x64\STEALERDLL.pdb age=212 guid=99F662A8-FCA0-49B6-B44E-FB7204BF0677]
  type=12 sizeOfData=20 pointerToRawData=119F24
  type=13 sizeOfData=852 pointerToRawData=119F38
  type=14 sizeOfData=0 pointerToRawData=0
PS C:\> ppee-cli.exe --debug C:\MalwareSamples\STEALERDLL.dll
C:\MalwareSamples\STEALERDLL.dll: 1282048 bytes, PE32+

Debug directory: 4 entrie(s)
  type=2 sizeOfData=71 pointerToRawData=119EDC [RSDS pdb=D:\Mktmp\StealerDLL\Release.x64\STEALERDLL.pdb age=212 guid=99F662A8-FCA0-49B6-B44E-FB7204BF0677]
  type=12 sizeOfData=20 pointerToRawData=119F24
  type=13 sizeOfData=852 pointerToRawData=119F38
  type=14 sizeOfData=0 pointerToRawData=0

Type 2 is CodeView (the PDB path, GUID and age), 12 VC feature, 13 POGO, 14 ILTCG. Compare explorer.exe: pdb=explorer.pdb age=1 plus a type-16 REPRO entry.

JSON: debug[] → type, timeDateStamp, sizeOfData, pointerToRawData, and for CodeView codeView (format, pdbPath, age, guid).

Hunting recipes

# PDB path, GUID and age
ppee-cli --json --debug f.exe | jq -r '.debug[].codeView | select(.) | "\(.pdbPath)\t\(.guid)\t\(.age)"'

# Build a PDB-path index for a sample set, then cluster by PDB file name
for f in samples/*; do
  ppee-cli --no-similarity --json --debug "$f" 2>/dev/null \
    | jq -r --arg f "$f" '.debug[]?.codeView | select(.) | "\(.pdbPath | split("\\") | last)\t\(.pdbPath)\t\($f)"'
done | sort > pdb-index.tsv
cut -f1 pdb-index.tsv | uniq -c | sort -rn | head

# PDB paths that contain a Windows user profile (possible attribution)
jq -r '.debug[]?.codeView.pdbPath // empty' < report.json | grep -i '\\Users\\'

# Files without any debug directory
for f in samples/*; do [ "$(ppee-cli --no-similarity --json --debug "$f" 2>/dev/null | jq '.debug | length')" = 0 ] && echo "$f"; done

Related: --debug · Rich header (toolchain fingerprint) · Similarity engine

References