Skip to content

Windows Flow-Integrity Mitigations: CFG and CET

How SafeSEH, SEHOP, ASLR, Control Flow Guard and hardware CET each close a Windows exploitation technique — what they check, how to enable them, and their limits.

Published on 3 min read

This is the third walkthrough in the Windows Exploitation area, written from the defender's side. The SEH overwrite and DEP bypass guides both ended at a set of Windows mitigations. Here we explain each one — what it checks, how to enable it, and where its limits are.

The layered answer to the two techniques

Windows exploitation history is a sequence of technique-then-mitigation, the same shape as the Linux and ARM areas:

TechniqueMitigation that answers it
SEH overwriteSafeSEH (compile-time) + SEHOP (runtime)
Stack shellcodeDEP (NX)
ROP / DEP bypassASLR (forces a leak), CFG (forward edge), CET (backward edge)
Fixed gadget addressesASLR (/DYNAMICBASE, /HIGHENTROPYVA)

SafeSEH and SEHOP

SafeSEH is compile-time: the linker builds a table of every valid exception handler in the module, and the dispatcher refuses to call a handler not in that table. The pop pop ret-into-shellcode trick fails because that address is not a registered handler.

SEHOP is runtime: it walks the SEH chain when an exception is dispatched and verifies it ends in the expected final record. A chain corrupted by an overflow no longer validates. Crucially, SEHOP covers modules built without SafeSEH, closing the third-party gap.

SafeSEH validates the handler; SEHOP validates the whole chain
exception raisedoverflow has corrupted a SEH record
dispatcher checks
SafeSEH: handler in table?reject unregistered handlers
and
SEHOP: chain intact?reject a broken chain
datacode

Control Flow Guard (CFG)

CFG protects the forward edge. The compiler (/guard:cf) marks valid indirect-call targets, and the OS keeps a bitmap of them; every indirect call is checked against the bitmap before it executes. A chain that calls into the middle of a function, or into data, is not a valid target and is blocked.

Its limits mirror forward-edge CFI on Linux: it is coarse-grained (any valid function entry is allowed, so valid-but-dangerous targets can remain reachable), it does not protect returns, and a non-CFG module in the process weakens it. That backward-edge gap is why CET exists.

Hardware CET: the shadow stack

Intel CET, opted into with /CETCOMPAT, adds a hardware shadow stack: a protected copy of return addresses checked at every ret. A ROP chain that overwrote return addresses on the normal stack no longer matches the shadow copy, and the process is terminated. CET is the backward-edge partner to CFG's forward edge — together they constrain both directions code reuse needs, exactly as PAC+BTI do on ARM.

What this teaches a defender

  • Enable the whole set, and check every module. /guard:cf, /DYNAMICBASE, /HIGHENTROPYVA, /NXCOMPAT, /SAFESEH (32-bit) and /CETCOMPAT each close a step above. The recurring weak link is a single third-party DLL missing one of them — audit the whole process, and use Exploit Protection to enforce per-process where you cannot rebuild.
  • Forward and backward edges are separate problems. CFG alone leaves returns open; CET alone leaves indirect calls open. Ship both on capable hardware.
  • Mitigations are cost multipliers, not cures. As on every platform, they make exploitation expensive and unreliable; the durable fix is preventing the memory-corruption bug — see memory-safe languages.

Key takeaways

  • SafeSEH (compile-time handler table) and SEHOP (runtime chain check) defeat the SEH overwrite; SEHOP covers non-SafeSEH modules.
  • CFG checks indirect-call targets against a bitmap (forward edge); it is coarse and does not protect returns.
  • Hardware CET adds a shadow stack (backward edge); CFG + CET together mirror ARM's BTI + PAC.
  • Enable the full flag set on every module; one unhardened DLL is the usual gap.

This completes the Windows Exploitation arc: SEH overwrites, DEP bypass with ROP, and the CFG/CET defenses. For the cross-platform picture, see the mitigation stack.

Related guides

0x9000 · Kernel Exploitation

KASLR, KPTI and Modern Kernel Defenses

Kernel address randomization, page-table isolation, and the config options that harden a Linux kernel against memory-corruption exploits — what each one stops and how to enable it.

0x8000 · ARM64 Exploitation

PAC and BTI: ARM's Answer to Code Reuse

Pointer authentication signs return addresses so a forged one faults; BTI forces indirect branches onto landing pads. How both work, how to enable and verify them, and their limits.

0x2000 · Exploit Mitigations

The Mitigation Stack: Which Defenses, In Order

Every exploit walks the same stages: bug, corruption, hijack, code execution, escalation. A defender's checklist of which mitigation stops each — across Linux, ARM, Windows and the kernel.