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
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) orcall [__guard_check_icall_fptr]followed bycall reg(x86) is a guarded indirect call. The target is in a register (raxon x64,ecxon x86), and Windows checks it against the CF function table before the call. Thexfgvariants also check the function's type.CFG call targetin 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 toCreateThread, … 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: placeslongjmpor 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¶
- Microsoft PE format specification, load configuration structure: every field of the load config directory.
- IMAGE_LOAD_CONFIG_DIRECTORY64 (Microsoft): the structure as declared in winnt.h.
- Control Flow Guard (Microsoft): what the CFG flag and Guard tables protect.
