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.
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:
| Technique | Mitigation that answers it |
|---|---|
| SEH overwrite | SafeSEH (compile-time) + SEHOP (runtime) |
| Stack shellcode | DEP (NX) |
| ROP / DEP bypass | ASLR (forces a leak), CFG (forward edge), CET (backward edge) |
| Fixed gadget addresses | ASLR (/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.
exception raisedoverflow has corrupted a SEH recordSafeSEH: handler in table?reject unregistered handlersSEHOP: chain intact?reject a broken chainControl 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/CETCOMPATeach 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.