Create your own
Lesson illustration

Comparing PE Security Properties Across Builds

Good progress: you now have two deliberately controlled x64 builds, matching PDBs, and embedded manifests. This lesson turns the build settings into verifiable artifacts. Rather than trusting CMake, compiler switches, or an IDE property page, you will compare what the linker was asked to do with what the final PE files actually contain.

By the end, you should be able to produce a concise, evidence-based comparison of NativeLabPlain.exe and NativeLabHardened.exe, separating:

  • build-time intent shown in the verbose linker command;
  • PE header and load-configuration metadata;
  • embedded manifest properties; and
  • conclusions that still require runtime validation.

This distinction matters throughout modern Windows exploitation. A PE header can show that an image advertises or contains mitigation support; it does not, by itself, prove the associated process policy is active for a particular launch.


1. Read a PE comparison as a chain of evidence

A useful comparison has three layers:

LayerEvidence sourceWhat it establishesWhat it does not establish
Build intentVerbose compiler/linker commandThe flags passed during this buildThat the output file is the expected artifact or that all metadata materialized
Static image metadatadumpbin, link /dump, PE-bearFields and tables embedded in that exact PE fileThat Windows enabled every corresponding runtime policy
Process behaviorWinDbg and process-mitigation queriesWhich protections apply to the running targetWhether every potential corruption is blocked

For this lesson, concentrate on the first two layers. Module 3 will make the runtime-policy question explicit.

The basic PE header properties you will inspect reside in several distinct locations:

  1. The COFF file header identifies architecture and broad image attributes.
  2. The optional header’s DLL Characteristics field contains loader-facing flags such as Dynamic Base, NX compatibility, high-entropy VA support, and the CFG-support bit.
  3. The Load Configuration Directory holds richer structures, including the /GS security-cookie pointer and CFG tables and flags.
  4. The executable’s resource section contains the embedded application manifest, which is where the Segment Heap request lives.

Do not merge those locations into one vague idea of “PE mitigations.” Their position tells you what kind of claim the value supports.

A `dumpbin /headers` excerpt showing the PE optional header’s decoded DLL characteristics. The highlighted “Control Flow Guard” line is a static image property, while Dynamic Base, High Entropy Virtual Addresses, and NX compatible are shown in the same field.

What the expected profiles imply

From the prior build lesson, both executables should show the same baseline properties:

  • AMD64 / PE32+ image;
  • Dynamic Base;
  • High Entropy VA;
  • NX compatible;
  • a security cookie entry where the compiler and linker emitted /GS support;
  • an embedded asInvoker manifest.

The hardened executable should additionally provide evidence of:

  • CFG support in the DLL Characteristics field;
  • CFG instrumentation and a function-ID table in Guard Flags;
  • any EH continuation metadata that was materialized for this particular code;
  • CET compatibility marking, as displayed by the installed toolchain’s dumpbin version;
  • SegmentHeap in its embedded manifest.

There is an important qualification for EH continuation protection: requesting /guard:ehcont is not the same as guaranteeing that a nonempty continuation table is needed for every source file. If the compiler produces no relevant continuation targets in this small target, you may see the linker flag in the build transcript without a meaningful table in the image. Record that observation accurately rather than treating it as a failure by default.


2. The PE fields behind the labels

Before inspecting output, establish what the labels mean. In the PE specification, the optional header’s “DLL Characteristics” field applies to EXEs as well as DLLs; the historical name is misleading.

PE Format - Win32 apps - Microsoft Learn

Read the relevant Microsoft PE Format reference to connect dumpbin labels with the actual PE fields. Focus on the difference between the compact DLL Characteristics bitfield and the richer Load Configuration Directory.

In the “DLL Characteristics” subsection, read the flag table. Identify the values for High Entropy VA, Dynamic Base, NX compatible, and Guard CF. Then find “The Load Configuration Structure (Image Only),” especially the “Load Configuration Layout” table. Locate SecurityCookie, GuardCFCheckFunctionPointer, GuardCFDispatchFunctionPointer, GuardCFFunctionTable, GuardCFFunctionCount, and GuardFlags. Read the GuardFlags list and note the distinct meanings of “CF Instrumented” and “CF Function Table Present.”

Two conclusions are especially important:

  • DYNAMICBASE does not mean “the program loaded randomly this time.” It means the image is relocatable and compatible with loader relocation. The actual load base is a runtime fact.
  • NXCOMPAT does not make all memory nonexecutable. It is an image compatibility declaration relevant to DEP behavior. Legitimate executable image sections still exist, and runtime policy remains relevant.
  • Guard CF in DLL Characteristics is not the whole CFG story. A robust static confirmation includes the Guard Flags and CFG table information from Load Config.

The CFG compiler and linker options work as a pair: compiler instrumentation supplies checks at relevant indirect transfers, while the linker emits and assembles supporting metadata.

/guard (Enable Control Flow Guard)

Read Microsoft’s /guard reference to anchor the static evidence in the compiler-and-linker model of CFG. This will keep you from equating a single header flag with full protection.

In “Remarks,” begin at the explanation of CFG checks and read the mitigation model. Focus on the fact that CFG constrains indirect control transfers but does not repair the underlying memory corruption. Continue through the build and verification guidance beginning “If you build using a single cl command.” Read the verification guidance. Note the recommended dumpbin /headers /loadconfig command and the expected CF Instrumented and FID table present evidence.

A compact interpretation guide:

Static evidenceCareful interpretation
Dynamic baseImage declares support for loader relocation; inspect base relocations and runtime mapping separately when needed.
High Entropy VAThe 64-bit image is compatible with higher-entropy ASLR. It does not measure the actual randomization distribution.
NX compatibleImage is compatible with DEP/NX policy. It does not describe every virtual-memory region.
Control Flow GuardThe PE’s DLL Characteristics field advertises CFG support. Confirm Load Config for instrumentation/table evidence.
CF InstrumentedThe image has CFG-related compiler/linker instrumentation metadata.
FID table presentThe Load Config contains a table of valid CFG function targets.
Security CookieThe image has a cookie pointer used by MSVC /GS; it does not prove coverage of every function or eliminate all stack corruption.
CET compatibilityAn image declaration of CET compatibility, not proof that a user-mode shadow stack is active.
SegmentHeap manifest entryA request for Segment Heap as the process default heap on supported Windows versions, not proof that every allocation in every module uses one allocator path.

3. Capture the linker’s own account of each build

Start in the x64 Native Tools Command Prompt for Visual Studio. Make a fresh verbose build transcript; this is your evidence of the exact command line passed to MSVC for the current artifacts.

mkdir C:\Lab\Records 2>nul

cd /d C:\Lab\Source\NativeLab

cmake --build --preset msvc-x64-debug --clean-first --verbose ^
  > C:\Lab\Records\NativeLab-build-verbose.txt 2>&1

Use --clean-first here because you are establishing a comparison baseline. It avoids relying on a stale object file built before a mitigation flag changed. It is not necessary for every later edit, but it is appropriate when recording the build profile for lab work.

Now filter the transcript for the profile-defining switches:

findstr /I ^
  /C:"/DYNAMICBASE" ^
  /C:"/HIGHENTROPYVA" ^
  /C:"/NXCOMPAT" ^
  /C:"/guard:cf" ^
  /C:"/guard:ehcont" ^
  /C:"/CETCOMPAT" ^
  /C:"/MANIFESTINPUT" ^
  C:\Lab\Records\NativeLab-build-verbose.txt

The exact visual formatting will vary with CMake and Ninja versions. Your task is to identify the link command for each named target, not merely find a switch somewhere in the overall transcript.

Record the following claims only when the relevant target’s command line supports them:

Build evidence to recordPlainHardened
/DYNAMICBASE, /HIGHENTROPYVA, /NXCOMPAT passed to linkerYesYes
/guard:cf supplied during compilation and linkingNoYes
/guard:ehcont supplied during compilation and linkingNoYes
/CETCOMPAT supplied to linkerNoYes
Per-target manifest supplied as linker inputPlain manifestHardened manifest

This is where a verbose build log has an advantage over a project file: it records what the build system actually invoked, after presets, generator logic, inherited defaults, and target-specific options were combined.


4. Compare the final executables with dumpbin

Now inspect the actual files that the build produced. In PowerShell, set paths once so that the commands remain readable:

$targetDir = 'C:\Lab\Targets\NativeLab\x64\Debug'
$recordDir = 'C:\Lab\Records'

$plain = Join-Path $targetDir 'NativeLabPlain.exe'
$hardened = Join-Path $targetDir 'NativeLabHardened.exe'

Get-FileHash $plain, $hardened -Algorithm SHA256

The hashes should differ. If they are identical, stop: you do not have two meaningfully distinct artifacts to compare.

Generate full header and load-configuration reports:

dumpbin /nologo /headers /loadconfig $plain |
    Set-Content -Encoding ASCII "$recordDir\NativeLabPlain.dumpbin.txt"

dumpbin /nologo /headers /loadconfig $hardened |
    Set-Content -Encoding ASCII "$recordDir\NativeLabHardened.dumpbin.txt"

dumpbin and link /dump use the same Microsoft parsing machinery for this purpose. As a cross-check, this should produce equivalent security-relevant content for the hardened target:

link /dump /headers /loadconfig C:\Lab\Targets\NativeLab\x64\Debug\NativeLabHardened.exe

The following short video shows the practical relationship between these tools and demonstrates a file-based header comparison workflow.

dumpbin.exe, link /dump, and the Portable Executable Format (PE Format)

Watch “dumpbin.exe, link /dump, and the Portable Executable Format (PE Format)” from AshleyPurringTech for a concise tool-oriented view of PE inspection and output comparison.

Watch tool orientation to see the relationship between dumpbin.exe and link /dump. Then watch header comparison for the workflow of saving dumpbin /headers output and comparing files. Apply that workflow to different mitigation profiles rather than to the video’s x86/x64 examples.

Inspect the output in context

Do not compare whole files blindly. Timestamps, image sizes, PDB paths, linker versions, and section layouts can make a raw diff noisy. Instead, open both reports side by side and inspect these areas in order:

  1. File Header Values

    • Both must identify machine (x64) or 8664 machine (x64).
    • Both should be PE32+ images.
    • This confirms that you are comparing like architectures.
  2. Optional Header Values / DLL characteristics

    • Both should list High Entropy Virtual Addresses, Dynamic base, and NX compatible.
    • Only the hardened output should list Control Flow Guard.
  3. Load Configuration

    • Look for a Security Cookie field in both builds.
    • In the hardened build, locate Guard Flags.
    • Confirm that the decoded Guard Flags include CF Instrumented and FID table present.
    • Locate the Guard CF function table and count, if shown by your dumpbin version.
    • Note any CET compatibility or EH continuation fields that your current linker emitted.
  4. Base Relocation Directory and .reloc

    • A dynamically relocatable image needs relocation information for the loader to apply when it cannot use the preferred base.
    • You do not need to manually decode relocation entries in this lesson. Simply ensure that the static ASLR claim is not being treated as an isolated bit with no surrounding context.

A targeted search makes navigation faster without replacing the full report:

$terms = @(
    'machine',
    'DLL characteristics',
    'Dynamic base',
    'High Entropy',
    'NX compatible',
    'Control Flow Guard',
    'Load Configuration',
    'Security Cookie',
    'Guard Flags',
    'CF Instrumented',
    'FID table',
    'EH Continuation',
    'CET'
)

Select-String -Path `
    "$recordDir\NativeLabPlain.dumpbin.txt",
    "$recordDir\NativeLabHardened.dumpbin.txt" `
    -Pattern $terms

For a direct line-oriented diff, use:

fc /n C:\Lab\Records\NativeLabPlain.dumpbin.txt C:\Lab\Records\NativeLabHardened.dumpbin.txt ^
  > C:\Lab\Records\Plain-vs-Hardened.dumpbin.diff.txt

Treat this diff as a navigation aid, not as the conclusion. For example, a changed timestamp is a difference but says nothing about exploitability; CF Instrumented is a difference with a clear security interpretation.


5. Verify the embedded manifest separately

The Segment Heap selection is not a normal DLL Characteristics flag and should not be inferred from dumpbin /headers. Inspect the final embedded manifest resource.

mt.exe -nologo -inputresource:"$plain;#1" `
    -out:"$recordDir\NativeLabPlain.extracted.manifest"

mt.exe -nologo -inputresource:"$hardened;#1" `
    -out:"$recordDir\NativeLabHardened.extracted.manifest"

Select-String `
    -Path "$recordDir\NativeLabPlain.extracted.manifest",
          "$recordDir\NativeLabHardened.extracted.manifest" `
    -Pattern 'requestedExecutionLevel', 'heapType', 'SegmentHeap'

The expected result is:

  • both manifests contain asInvoker;
  • only NativeLabHardened contains:
<heapType>SegmentHeap</heapType>

This gives you a complete static comparison: headers and Load Config for linker-emitted image properties, plus the embedded manifest for the process-default heap request.


6. Write the comparison as defensible conclusions

Add a section like this to the lab record. Fill it with your exact hashes and observed wording rather than copying an expected result uncritically.

Comparison date:
OS build:
MSVC / linker version:

Plain executable:
  Path:
  SHA256:
  Architecture:
  Dynamic Base:
  High Entropy VA:
  NX compatible:
  Guard CF DLL characteristic:
  Security Cookie field:
  Guard Flags:
  Embedded heapType:

Hardened executable:
  Path:
  SHA256:
  Architecture:
  Dynamic Base:
  High Entropy VA:
  NX compatible:
  Guard CF DLL characteristic:
  Security Cookie field:
  Guard Flags:
  Guard CF function-table evidence:
  EH continuation evidence:
  CET compatibility evidence:
  Embedded heapType:

Build-log correlation:
  Plain linker command:
  Hardened linker command:
  Profile-defining differences:

Static conclusion:
  Both targets are native x64 PE32+ desktop images that retain ASLR- and
  DEP-related compatibility declarations. NativeLabHardened additionally
  contains CFG-related PE evidence and requests Segment Heap through its
  embedded manifest.

Runtime limitation:
  These static findings do not establish whether CFG, CET shadow stacks,
  ACG, CIG, or any other process mitigation was enabled for a particular
  launch. Runtime policy must be inspected separately.

A few mismatch patterns are worth recognizing:

ObservationLikely interpretationImmediate response
Hardened linker options absent from verbose logBuild configuration was not applied to the targetCheck target-specific CMake options and rebuild cleanly.
Linker options present, but you inspected an old binaryArtifact-selection errorRecheck paths, timestamps, and hashes.
Guard CF appears but Guard Flags lack expected CFG evidenceInvestigate tool version, stale build products, and whether compiler/linker options reached the intended stagesPreserve the output; do not overstate CFG coverage.
No EH continuation table in this small targetThe requested option may have had no relevant continuation metadata to emitRecord build intent and observed absence; do not call it equivalent to runtime protection.
Segment Heap appears only in source manifest, not extracted executable manifestManifest embedding failed or the wrong output was inspectedFix this before heap experiments.

Key takeaways

You now have a repeatable method for comparing Windows PE security properties without confusing configuration with evidence:

  • Use a verbose build transcript to establish which compiler and linker flags were actually supplied.
  • Use dumpbin /headers /loadconfig or link /dump to inspect the final PE’s architecture, DLL Characteristics, Load Configuration, security cookie, and CFG metadata.
  • Confirm CFG through both the Guard CF characteristic and the Load Config’s Guard Flags and function-table evidence.
  • Inspect the embedded manifest with mt.exe; Segment Heap is a manifest-level request, not a generic PE header flag.
  • State static results precisely: a mitigation-compatible or mitigation-instrumented image is not necessarily a process where the OS has enforced every relevant runtime defense.

Next, you will move from image metadata to the loaded process: extracting process-mitigation policy state and correlating it with the PE load-configuration properties you identified here.

Can't find a good explanation? Sign up and we'll make it for you

Sign up