Skip to content

Load Config Directory

Data directory 10

IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG: DataDirectory[10] in the Optional header. All data directories

The load configuration directory (IMAGE_LOAD_CONFIG_DIRECTORY32/64) is where the compiler and linker leave the metadata for Windows' exploit mitigations: the /GS stack cookie, SafeSEH, Control Flow Guard (CFG), CET shadow-stack continuation targets and more. It tells a vulnerability researcher which mitigations a module actually has, and it tells a malware analyst which toolchain built the file, or that the file is missing everything a normal compiler would have produced.

Key fields

Field / table Meaning
Size Size of the structure. It grows with each Windows SDK, so it hints at the linker version
SecurityCookie VA of the /GS stack cookie. 0 means no stack-cookie protection is registered
SEHandlerTable / SEHandlerCount → Safe SEH x86 only: the whitelist of valid SEH handlers (/SAFESEH)
GuardCFCheckFunctionPointer / GuardCFDispatchFunctionPointer Pointers the loader patches to the CFG check routines
GuardCFFunctionTable / Count → Guard CF function table Every valid indirect-call target (the .gfids data)
Guard address-taken IAT Imports whose address is taken (not only called)
Guard long-jump targets Valid longjmp destinations
Guard EH continuation Valid exception-continuation targets for CET shadow stacks (/guard:ehcont)
GuardFlags Which of the above exist, plus the table stride in the top 4 bits
GlobalFlagsSet/Clear, ProcessHeapFlags Override NtGlobalFlag / heap behavior for this image
Volatile Metadata ARM64EC / x64 volatile access and info-range tables

GuardFlags bits

Bit Name Meaning
00000100 CF_INSTRUMENTED Compiled with CFG checks
00000200 CFW_INSTRUMENTED CF + write integrity checks
00000400 CF_FUNCTION_TABLE_PRESENT The target table exists
00000800 SECURITY_COOKIE_UNUSED Not using /GS
00001000 PROTECT_DELAYLOAD_IAT Delay-load IAT is protected (read-only)
00002000 DELAYLOAD_IAT_IN_ITS_OWN_SECTION Delay-load IAT in its own section
00004000 CF_EXPORT_SUPPRESSION_INFO_PRESENT Export-suppression information present
00008000 CF_ENABLE_EXPORT_SUPPRESSION Export suppression enabled
00010000 CF_LONGJUMP_TABLE_PRESENT longjmp target table present
00020000–00080000 RF_INSTRUMENTED / RF_ENABLE / RF_STRICT Return Flow Guard (retired)
00100000 RETPOLINE_PRESENT Built with retpoline
00400000 EH_CONTINUATION_TABLE_PRESENT CET EH continuation table present
00800000 XFG_ENABLED eXtended Flow Guard (/guard:xfg)
01000000 / 02000000 CASTGUARD / MEMCPY_PRESENT CastGuard / guarded memcpy
F0000000 (mask) Table stride Extra bytes per CF table entry, e.g. 1 = a flags byte after each RVA

What PPEE shows

Some views are GUI-only

The Dynamic Value Relocation Table, the Enclave Configuration and the Volatile tables below are browsed and edited in the GUI. The CLI's --loadconfig reports the header fields and the guard tables.

  • DIR_ENTRY_LOAD_CONFIG lists every header field (Member · Value · Comment). GuardFlags is expanded into its individual flags, and in the GUI it has a checkbox picker.
  • Child nodes for each table that exists: Safe SEH (n) (handler RVAs), Guard CF function table (n), address-taken IAT (n), long-jump targets (n), EH continuation (n) (RVA + per-entry flags), and Volatile Metadata (access table, info ranges).
  • Every RVA resolves to its section, and Follow in Hex View (Ctrl+H) works on each entry.

Windows screenshot: Load Config of a NativeAOT ransomware: a security cookie, GuardFlags CF_INSTRUMENTED only and no CF function table

Load Config of a NativeAOT ransomware: a security cookie, GuardFlags CF_INSTRUMENTED only and no CF function table

A NativeAOT ransomware (d1337711….exe). SecurityCookie is set (__security_cookie, so /GS is on), and the CFG pointers resolve to __guard_check_icall_fptr and __guard_fids_table. But GuardFlags expands to CF_INSTRUMENTED alone, GuardCFFunctionTable and GuardCFFunctionCount are zero, and the Optional header has no GUARD_CF: Control Flow Guard is not active for this image, the same profile as the x86 app row in the table below.

Dynamic Value Relocation Table (DVRT)

The DVRT (DynamicValueRelocTable in the header) lists code patches the loader applies while mapping the image, such as call-sequence fixups and the ARM64X hybrid-image conversions. Microsoft doesn't document the format; PPEE decodes the layouts from the Windows SDK headers.

  • Header: version and size, then the entries. Version 1 entries hold relocation blocks (like the base relocation blocks); version 2 entries hold fixup information.
  • Symbols decoded: 3 import control transfer, 4 indirect control transfer, 5 switch-table branch, 6 ARM64X (variable-length records that can even patch the file header, such as FileHeader.Machine), and 8 ARM64 kernel import call transfer. Symbols 1, 2 (return-flow prologue/epilogue) and 7 (function override), and version 2 fixup info have no public record layout and are shown as a raw, followable byte range.
  • Records show their RVA and the decoded bit columns, which are editable (each edit is a bit-range write; a value too wide for its bits is refused).
  • Highlights: bad versions, sizes, symbols, blocks and records are marked with warnings and errors.

DVRT normally appears in Windows system components and hybrid ARM64X images. A DVRT in a third-party file is unusual and worth reading, because it is a mechanism for changing code at load time.

Enclave Configuration

A separate Enclave Configuration node shows the enclave configuration that enclave images carry: Size, MinimumRequiredConfigSize, PolicyFlags (decoded, for example whether the enclave permits debugging), the import list (NumberOfImports, ImportList, ImportEntrySize), the 16-byte FamilyID and ImageID, ImageVersion, SecurityVersion, EnclaveSize, NumberOfThreads and the decoded EnclaveFlags. It is rare outside enclave DLLs.

Volatile Metadata

The Volatile Access Table and Volatile Info Table (ARM64EC/x64) give each range a Size column as well as Range From and Range To (display-only: From + Size). Every editable cell works with Follow in Hex View.

Control Flow Guard in the code

With CFG, the compiler routes every indirect call (a call through a function pointer or a vtable) through a check. The disassembly shows it by name:

$ ppee-cli --disasm ep --count 400 appxsip.dll | grep -E "guard|CFG"
   0000000180035030  48 89 5C 24 08        mov qword ptr [rsp+0x8], rbx             ; CFG call target, XFG hash 0xA440AE23305F3A71
   0000000180035386  FF 15 D4 2D 00 00     call qword ptr [__guard_xfg_dispatch_icall_fptr]

(appxsip.dll from the Windows 11 SDK, x64.)

  • call [__guard_dispatch_icall_fptr] (x64) or call [__guard_check_icall_fptr] followed by call reg (x86) is a guarded indirect call. The target is in a register (rax on x64, ecx on x86), and Windows checks it against the CF function table before the call. The xfg variants also check the function's type.
  • CFG call target in the comment: this address is in the CF function table, so it may be called indirectly. That makes it a callback, a vtable method, a function passed to CreateThread, … The list is a free source of function starts, and PPEE's code scan uses it as one. On x86, which has no .pdata, it is often the largest one.
  • XFG hash 0x…: with /guard:xfg, the 8 bytes before each such function hold a hash of its type. Two functions with the same hash have the same signature, which helps match a function pointer to the functions it can call.
  • (suppressed), (suppressed: export): the function is in the table but must not be called indirectly. EH handler: a language exception handler.
  • CFG longjmp target, EH continuation target: places longjmp or an exception handler may resume at (the CET shadow stack checks the second). CFG address-taken import: a call through an import whose address the code also takes.

Reading a packer or a loader

A function that a CFG-enabled file calls indirectly but that is not a CFG call target makes Windows end the process with a CFG violation. Code that someone added to a CFG-built file therefore has to be reached in other ways, or has to switch CFG off. The Code analysis warns when a CFG-built file's entry point or a TLS callback is not in the table: the linker always puts them there, so the file was changed after linking.

Reading load config like an analyst

Observation What it suggests
No load config at all on a recent MSVC-looking binary Built with another toolchain (Go, MinGW, Delphi, Nim, Rust-GNU), or stripped by a packer or protector
SecurityCookie = 0 No /GS stack protection registered, which is interesting for exploit research
GUARD_CF requested in DllCharacteristics but no CF function table CFG is requested but not implemented; the module is a CFG bypass candidate (indirect calls go unchecked)
Large CF table and EH-continuation table Modern, fully hardened MSVC build
x86 file with Safe SEH handlers Built with /SAFESEH. Without it, any handler address is accepted (a classic SEH overwrite target)
GlobalFlagsSet non-zero The image asks for special NtGlobalFlag behavior (heap checking, …). Rare in normal software
The CF table used as a function list Every indirect-call target is listed: a free source of function starts for stripped binaries (callbacks, vtable methods)

CLI and JSON

$ ppee-cli --loadconfig explorer.exe
LoadConfig (PE32+):
  Size=118 TimeDateStamp=0 Version=0.0
  EditList=0 SecurityCookie=140431C68
  GuardCFCheckFunctionPointer=1403A0280 GuardCFDispatchFunctionPointer=1403A0288
  GuardCFFunctionTable=1403A097C GuardCFFunctionCount=F71 GuardFlags=417500
  GuardCF function table (3953):
    RVA=4F10

JSON: loadConfig → present, isPe64, header (every field, hex), safeSeh, guardCFFunction, guardAddressTakenIat, guardLongJumpTarget, guardEHContinuation: each {present, entries[{rva}]}.

Hunting recipes

# Mitigation profile of one file
ppee-cli --json --headers --loadconfig f.exe | jq '
  def h: ascii_downcase | explode | reduce .[] as $c (0; . * 16 + (if $c >= 97 then $c - 87 else $c - 48 end));
  def bit($n): (. / $n | floor) % 2 == 1;
  (.headers["OptionalHeader.DllCharacteristics"] | h) as $dc | (.loadConfig.header.guardFlags | h) as $gf
  | {loadConfig: .loadConfig.present, gsCookie: (.loadConfig.header.securityCookie != "0"),
     safeSeh: (.loadConfig.safeSeh.entries | length),
     cfgRequested: ($dc | bit(16384)), cfgTargets: (.loadConfig.guardCFFunction.entries | length),
     cfgBypassCandidate: (($dc | bit(16384)) and (.loadConfig.guardCFFunction.entries | length) == 0),
     ehContinuation: ($gf | bit(4194304)), longjmpTable: ($gf | bit(65536)), xfg: ($gf | bit(8388608))}'

# Modules in a folder that are NOT CFG-instrumented (exploit-research triage)
for f in /mnt/win/System32/*.dll; do
  ppee-cli --no-similarity --json --loadconfig "$f" 2>/dev/null \
    | jq -r --arg f "$f" 'select((.loadConfig.guardCFFunction.entries | length) == 0) | $f'
done

# CFG targets as a function-start list
ppee-cli --json --loadconfig f.exe | jq -r '.loadConfig.guardCFFunction.entries[].rva'

Related: --loadconfig · Hardening flags · CI hardening gate · Exception directory

References