The previous tutorials in this series defeated DEP with ROP and ASLR with leaks or non-randomized modules. Both techniques assumed the attacker could redirect indirect control flow (vtable hijacks, function-pointer overwrites, return-address rewrites) to arbitrary executable bytes inside a loaded module. That assumption no longer holds on a properly configured Windows 11 binary. Two layered mitigations, Control Flow Guard (CFG, software, since Windows 8.1) and Intel CET shadow stacks plus IBT (hardware, since Windows 10 20H2 and Windows 11), close most of the doors that ROP and JOP relied on.
This tutorial covers what each mitigation does, what it doesn’t catch, the bypass primitives that exist in the wild, and how to audit whether a binary is actually compiled with all of them. It is not a “how to defeat CFG” walkthrough; the bypasses are summarized at a structural level so you can recognize them when reading public exploits.
The mitigation stack on modern Windows
Modern Windows user-mode exploit mitigations form a stack. Each layer addresses a different class of attack, and each can be bypassed in isolation given the right primitive, but the combination is what makes practical exploitation expensive.
+-----------------------------------------------------------+
| Stack canaries — detect linear stack overwrites |
+-----------------------------------------------------------+
| ASLR + HEASLR — force a memory-disclosure leak |
+-----------------------------------------------------------+
| DEP / NX — no W+X pages (forces ROP) |
+-----------------------------------------------------------+
| CFG — validates indirect CALLs at run- |
| time against a compiler-emitted |
| bitmap of valid function entries |
+-----------------------------------------------------------+
| CET-SS (hardware) — per-thread shadow stack; CPU |
| compares RET target to a copy |
| the attacker cannot write |
+-----------------------------------------------------------+
| CET-IBT (hardware) — every indirect branch must land |
| on an ENDBR64 marker |
+-----------------------------------------------------------+
| ACG — no new RWX pages, no JIT in |
| hardened processes |
+-----------------------------------------------------------+Each layer addresses a different attack class:
- ASLR forces the attacker to leak an address before they can use any absolute pointer.
- DEP forces them to reuse code already in memory: ROP or JOP.
- CFG closes off the JOP-style indirect-call-to-mid-function path. Indirect calls go to function entries only.
- CET-SS closes off the classical ROP path. Returns must match a hardware-protected copy of the call stack.
- CET-IBT closes off whatever indirect-branch primitives survive CFG, by requiring an ENDBR64 byte at the target.
- ACG prevents the attacker from allocating fresh executable memory after the fact.
Microsoft’s earlier user-mode mitigation effort, the Enhanced Mitigation Experience Toolkit (EMET), shipped many of these ideas as out-of-band injection: SEHOP, EAF, EAF+, ASR, mandatory ASLR, anti-ROP heuristics. EMET reached end-of-life in 2018 because nearly all of its protections were folded directly into Windows 10 and 11 as Exploit Protection (the Get-ProcessMitigation / Set-ProcessMitigation PowerShell surface) and into the compiler/linker toolchain (CFG, /GS, /CETCOMPAT). The result is a more durable, non-bypassable-by-uninstall set of mitigations applied by default to in-box binaries.
Note
The mitigations in this tutorial are configured per-process and per-image. A process whose main image enables CFG and CET-SS can still load a non-compliant DLL whose code lacks the relevant metadata, and that DLL becomes a soft target. Auditing requires checking every loaded module, not just the EXE.
CFG: how it works
Control Flow Guard is a compiler/linker/loader/runtime cooperation. The pieces:
- Compiler. When emitting an indirect call (
call rax,call qword ptr [rcx+0x18], virtual dispatch),cl.exe /guard:cfrewrites the call site to first check the target. - Linker.
link.exe /guard:cfemits a guard CF function table into the PE’s load config directory listing every valid indirect-call target in the image (function entries, exported functions, vtable slots). - Loader. When the image loads,
ntdll!LdrpProtectAndRelocateImagepopulates a process-global CFG bitmap in shared memory. Each bit covers an 8-byte slot of the address space and is set when that slot is a valid function entry per the load config. - Runtime. Every CFG-checked indirect call goes through
_guard_dispatch_icall(or, on older toolchains,__guard_check_icall_fptr), which looks up the target’s bit in the bitmap. If the bit is clear, the dispatcher callsRtlFailFast2(FAST_FAIL_GUARD_ICALL_CHECK_FAILURE)and the process dies.
The bitmap is enormous in principle (one bit per 8 bytes across the entire 64-bit address space) but Windows uses a sparse two-level structure so only mapped regions consume real memory. The bitmap pages are mapped read-only into every process and updated by the kernel as DLLs load.
The check at the call site
Before CFG, an indirect call is one instruction:
; Without CFG
mov rax, [rcx+0x18] ; load function pointer (e.g., vtable slot)
call rax ; jump there, no validationWith CFG enabled, the same call site looks like one of two patterns depending on toolchain version. Older toolchains use the explicit-check form:
; With CFG (older / debug, "check" variant)
mov rax, [rcx+0x18]
mov rcx, rax ; arg to the check function
call qword ptr [__guard_check_icall_fptr] ; aborts on bad target
call rax ; only reached if check passedNewer toolchains use the optimized dispatcher form, which is the dominant pattern in shipping Windows 11 binaries:
; With CFG (current, "dispatch" variant)
mov rax, [rcx+0x18]
call qword ptr [_guard_dispatch_icall_fptr]
; Note: no second 'call rax'. The dispatcher tail-calls the target itself
; after validating it, so control transfer is fused with the check.The dispatcher form is faster (one indirect call instead of two) and slightly harder to bypass because there is no window between “check” and “call” where the attacker could overwrite the target.
What _guard_dispatch_icall actually does
Roughly, in pseudocode:
void _guard_dispatch_icall(void *target /* in rax */) {
if (LdrSystemDllInitBlock.CfgBitMap == NULL) {
// CFG not initialized in this process; pass through
((void(*)(void))target)();
return;
}
uintptr_t bit_index = ((uintptr_t)target) >> 3;
uint8_t byte = CfgBitMap[bit_index >> 3];
uint8_t bit_mask = 1 << (bit_index & 7);
if ((byte & bit_mask) == 0) {
// Suppressed export? Bit 0 of the low byte of the target is checked
// against the "address taken" semantics; for a bad target, fail.
RtlFailFast2(FAST_FAIL_GUARD_ICALL_CHECK_FAILURE);
// does not return
}
// Optional alignment check on the low bits of the target (8-byte aligned
// function entries by default; relaxed on some images).
((void(*)(void))target)(); // tail call
}The exact dispatcher is hand-written assembly with several fast paths (short forward branches, a per-process function-pointer trampoline, a check that skips the bitmap entirely when CFG is disabled in the process), but the validation rule is “lookup the 1-bit-per-8-bytes bitmap; if the bit is clear, FAIL_FAST.”
Note
The actual symbols you will see in WinDbg vary.
_guard_check_icall_fptrand_guard_dispatch_icall_fptrare pointers in the import directory of every CFG-enabled image; they resolve at load time tontdll!LdrpValidateUserCallTarget(orntdll!LdrpDispatchUserCallTargetfor the optimized variant). On a CFG-disabled image, the loader points them at a stub that just returns, so the check is a no-op.
Inspecting CFG metadata in a binary
dumpbin /loadconfig shows the CFG-related fields of IMAGE_LOAD_CONFIG_DIRECTORY64:
> dumpbin /loadconfig notepad.exe
...
GuardCFCheckFunctionPointer = 1407FE3D8
GuardCFDispatchFunctionPointer = 1407FE3E0
GuardCFFunctionTable = 14001A000
GuardCFFunctionCount = 1A4
GuardFlags = 00010500
CF Instrumented
CF Function Table Present
Long jump targets present
...GuardFlags bits worth knowing:
IMAGE_GUARD_CF_INSTRUMENTED (0x100), the image was compiled with/guard:cf.IMAGE_GUARD_CFW_INSTRUMENTED (0x200), write integrity checks (separate feature).IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT (0x400), the function table is populated.IMAGE_GUARD_RF_INSTRUMENTED (0x00020000), the image has Return Flow Guard metadata (deprecated, replaced by CET-SS).IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT (0x00400000), exception-handler continuation targets are validated.
If CF Instrumented is not present, the binary is not protected, regardless of process policy.
What CFG catches
CFG specifically validates indirect calls. The attack patterns it breaks:
- Indirect call to attacker-chosen mid-function bytes. Classic JOP (“jump-oriented programming”) chains stitched gadgets out of mid-function instruction sequences ending in
jmp regorcall reg. Those mid-function offsets aren’t in the CFG bitmap, so the call fails fast. - Vtable hijack to a non-function. A use-after-free or type-confusion that places an attacker-controlled pointer in a vtable slot is caught when the C++ virtual call dispatches: the slot now points at non-function memory (the heap, a string, a fake vtable). Bit clear, FAIL_FAST.
- Function-pointer overwrites in writable memory. Callbacks, dispatch tables, and
__cdeclindirect callees stored in.dataor on the heap, when overwritten with an arbitrary attacker-supplied address, are caught at the call site. - Stack pivots into JOP. Even if the attacker pivots
rspinto attacker-controlled data, eventually the chain hits acall qword ptr [...]which goes through the dispatcher. The first non-function target ends the chain.
The common thread: CFG raises the cost of “I have an arbitrary write or arbitrary read-then-write” from “go anywhere” to “go to one of the N valid function entries already exported by some loaded module.”
What CFG does NOT catch
Equally important is what CFG ignores:
- Returns.
retis not an indirect call. The dispatcher does not run onret. Classical ROP, where the attacker writes a sequence of return addresses on the stack and letsretafterretchain through gadgets, is entirely uncovered by CFG. This is precisely why CET-SS was needed. - Direct jumps and calls.
jmpto an absolute address baked into instruction bytes, andcall rel32to a relative target inside the same module, are unchanged. The attacker has to overwrite a pointer to redirect them, which usually means an indirect call, but a direct chain inside a corrupted code region is invisible to CFG. - Function pointers overwritten with valid CFG targets. If you redirect a callback to
systemorWinExecor any other valid exported function, CFG happily allows the call. CFG’s job is to enforce “this address is some function entry,” not “this address is the right function entry.” - Memory corruption that doesn’t redirect control flow. Arbitrary read, info leaks, type confusion that returns attacker data instead of validated data, integer overflows that grant unintended privileges. All untouched by CFG.
- Code paths before CFG bitmap is loaded. Extremely rare in practice (only the loader’s earliest startup), but corner-case primitives that fire before
LdrpInitializeProcesscan theoretically execute uninstrumented indirect calls. - CFG-suppressed exports. Some exports are deliberately marked as not-callable indirectly (
IMAGE_GUARD_FLAG_FID_SUPPRESSED); a few are deliberately marked as exempt for legacy reasons. The set is small but real.
CFG bypass primitives that exist in the wild
The published bypass classes have been remarkably stable since 2015. They are not new every quarter; they are structural.
Find a useful valid CFG target
The cheapest bypass is to redirect the indirect call to a function that is in the CFG bitmap and whose semantics give the attacker what they want. Examples that have shown up in real exploits:
- A wrapper that calls a writable callback. Some Windows internal functions accept a pointer to a struct, then call a function pointer stored inside it. If the wrapper is a valid CFG target and the attacker controls the struct, the wrapper does the indirect-to-anywhere call on the attacker’s behalf, often without going through the CFG-protected dispatcher because the inner call path was inlined or wasn’t instrumented.
LoadLibrary/GetProcAddresspatterns. Calling these with attacker-controlled strings is functionally equivalent to running attacker-chosen code.WinExec/system. Direct command execution, no shellcode needed.VirtualProtectfollowed by a return into shellcode. Useful when CET-SS is not enforced and the attacker only needs to flip a page to executable.
The exploit author’s job is to find such a function in the CFG-allowed set, line up its arguments through whatever gadget infrastructure they can reach with writes alone, and call it.
Modify the CFG bitmap
The CFG bitmap is mapped read-only into every process. With a sufficient kernel primitive or a bug in the CFG manager, an attacker can remap or unmap pages of the bitmap to mark arbitrary addresses as “valid.” There have been several historical patches in this area, and the approach is now firmly in the “kernel bug needed” category for typical user-mode targets.
Disable CFG via SetProcessValidCallTargets
There is a Windows API, SetProcessValidCallTargets, that adds entries to a process’s CFG bitmap dynamically. It requires PROCESS_SET_INFORMATION access and is itself protected (the process must have asked for dynamic code mitigation off, or be running with appropriate privileges). Bugs in callers of this API have provided one-shot bypasses where attacker-controlled data ended up in the “valid targets” list.
Stack pivot, then ret
The simplest CFG bypass on a non-CET system: get one indirect call into a mov rsp, X ; ret gadget where X ends up pointing at an attacker-controlled ROP chain. The single indirect call goes through CFG; if the gadget itself is at a valid CFG entry (which is sometimes the case, since ret is also at function-prologue boundaries), the dispatcher passes it. From that point on, every transfer is ret, and ret is not checked. Classical ROP resumes.
This is exactly the gap that CET-SS was designed to close.
xfg (eXtended Flow Guard)
Microsoft began rolling out a stricter variant called xfg (eXtended Flow Guard) in Windows 10 builds and into Windows 11. xfg adds a signature hash to each indirect call site and to each candidate function. At call time, the dispatcher checks not only that the target is in the bitmap, but that the target’s prologue carries the same hash as the call site.
The hash is derived from the function’s signature (parameter types, return type), so a void(*)(int) call site can only target functions with void(int) shape. This narrows the bypass set dramatically: even if system is in the bitmap, its signature is unlikely to match the call site you control.
Practical caveats as of 2026:
- xfg is enabled gradually. It is not on by default for arbitrary user binaries; it is enabled per-image at compile time with
/guard:xfgand the corresponding linker flag, and only in some Windows-shipping binaries. - All callees of an xfg call site must also be xfg-instrumented. Mixed binaries fall back to plain CFG semantics for affected sites.
- xfg metadata format has shifted across builds; treat it as an evolving target rather than a stable mitigation to plan exploitation around.
For the purposes of this tutorial, treat xfg as “an additional cost that may or may not be present.” Audit specific binaries with dumpbin /loadconfig (the GuardFlags field reports xfg-related bits in newer toolchains) and Get-ProcessMitigation.
CET shadow stack on Windows
Intel Control-flow Enforcement Technology (CET) is a CPU feature shipping in Tiger Lake and newer Intel parts and Zen 3 and newer AMD parts. It has two halves:
- Shadow Stack (SHSTK / CET-SS). A second, hardware-managed stack. Every
callpushes the return address onto both the regular stack and the shadow stack. Everyretpops from both and traps if they don’t match. The shadow stack is mapped with a special page-table attribute so user code cannot write to it via normal stores; onlycall,ret, and a few privileged instructions touch it. - Indirect Branch Tracking (IBT). Every indirect
jmporcallmust land on a special instruction (ENDBR64on x86-64) whose only purpose is to be a valid landing pad. Landing on anything else raises#CP.
Windows exposes CET-SS to user mode under the marketing name “Hardware-Enforced Stack Protection.” It was introduced for kernel-mode in Windows 10 20H2, and as a per-process opt-in for user mode in Windows 11. Windows 11 22H2 strengthened the user-mode story by enabling strict mode for more in-box binaries.
Modes
A process’s CET-SS configuration has three states. They are exposed via SetProcessMitigationPolicy with the ProcessUserShadowStackPolicy policy:
typedef struct _PROCESS_MITIGATION_USER_SHADOW_STACK_POLICY {
union {
DWORD Flags;
struct {
DWORD EnableUserShadowStack : 1; // shadow stack is allocated
DWORD AuditUserShadowStack : 1; // log mismatches, no kill
DWORD SetContextIpValidation : 1;
DWORD AuditSetContextIpValidation : 1;
DWORD UserShadowStackStrictMode : 1; // kill on mismatch (strict)
DWORD BlockNonCetBinaries : 1;
DWORD BlockNonCetBinariesNonEhcont: 1;
DWORD AuditBlockNonCetBinaries : 1;
DWORD CetDynamicApisOutOfProc : 1;
// ...
DWORD ReservedFlags : 22;
};
};
} PROCESS_MITIGATION_USER_SHADOW_STACK_POLICY;The three states a developer cares about:
- Disabled. No shadow stack allocated. Returns are validated only by
/GScanaries. - Compatible / Audit. Shadow stack is allocated; mismatches are logged to ETW (Microsoft-Windows-Threat-Intelligence) but the process is not killed. This is the default for many in-box binaries during the rollout.
- Strict. Shadow stack is allocated; any mismatch triggers
RtlFailFast2(FAST_FAIL_INVALID_SHADOW_STACK)and the process dies. Strict mode is opted in via/CETCOMPAT(linker flag, sets the image bitIMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT) and/or/CETCOMPATSTRICT. Lowercase variants/cetcompatand/cetcompatstrictwork as well.
Note
Audit mode is real and meaningful: it lets Microsoft and ISVs ship CET-aware binaries that don’t crash on legacy plug-ins or third-party DLLs that haven’t been recompiled. It also means the “this exploit ran” telemetry exists even when the exploit succeeded.
Per-thread shadow stack layout
Each thread on a CET-enabled process gets its own shadow stack. Layout, roughly:
+------------------------------+ high addresses
| guard page (no access) |
+------------------------------+
| shadow stack region |
| (size ~ 1/8 of normal stack |
| on 64-bit) |
| |
| [SSP grows downward as |
| calls are nested] |
+------------------------------+
| guard page (no access) |
+------------------------------+ low addressesThe SSP register (SSP / IA32_PL3_SSP MSR shadow) points at the current top. The pages backing the shadow stack are mapped with the special “shadow stack” attribute, so a normal mov [rsp_shadow], rax faults; only call, ret, wrss (when enabled), and incssp modify it from user code.
WRSS (write to shadow stack) and WRUSS (write to user shadow stack from kernel) are the privileged knobs that let the OS or, in rare opted-in cases, the application itself, modify the shadow stack. Most processes have WRSS disabled; an exploit primitive that writes to the shadow stack therefore typically needs a kernel bug or a dynamic code primitive that flips IA32_U_CET.WRSS_EN.
A tiny program that crashes on a synthetic ret-overwrite
To see CET-SS fire, compile this with strict CET and run it under WinDbg:
// cet_demo.c
// Build: cl /O2 /GS /guard:cf cet_demo.c /link /CETCOMPAT /CETCOMPATSTRICT
#include <stdio.h>
#include <windows.h>
__declspec(noinline)
static void inner(void) {
char buf[16];
// Find our return address on the stack and clobber it.
// On x86-64 the return address is just above the saved RBP / locals.
// This is a synthetic "I have a stack overflow" simulation.
void **frame = (void **)_AddressOfReturnAddress();
printf("inner(): return address slot = %p, currently = %p\n",
frame, *frame);
*frame = (void *)(uintptr_t)0xdeadbeefcafef00d;
// ret will now load 0xdeadbeefcafef00d into rip.
// CET-SS compares against the shadow-stack copy and traps.
}
int main(void) {
PROCESS_MITIGATION_USER_SHADOW_STACK_POLICY p = {0};
SIZE_T sz = sizeof(p);
GetProcessMitigationPolicy(GetCurrentProcess(),
ProcessUserShadowStackPolicy, &p, sz);
printf("Enabled=%u Strict=%u\n",
p.EnableUserShadowStack, p.UserShadowStackStrictMode);
inner();
printf("returned (should not see this with strict CET)\n");
return 0;
}Running it on a 12th-gen-or-newer Intel host with the binary marked /CETCOMPATSTRICT:
> cet_demo.exe
Enabled=1 Strict=1
inner(): return address slot = 000000DCB4DFFB28, currently = 00007FF6A8B4124C
[process exits with FAST_FAIL_INVALID_SHADOW_STACK]In WinDbg under g:
(2480.310c): Security check failure or stack buffer overrun - code c0000409 (!!! second chance !!!)
Subcode: 0x39 FAST_FAIL_INVALID_SHADOW_STACK
rax=00000000c0000409 rbx=0000000000000000 rcx=0000000000000039
rsp=000000dcb4dffb20 rip=00007ff6a8b4124d
ntdll!RtlFailFast2+0x9Subcode 0x39 (FAST_FAIL_INVALID_SHADOW_STACK) is the unmistakable signature of a CET-SS mismatch on Windows. If you see this on a real crash, CET-SS caught a return-address tampering primitive.
Bypass primitives for CET-SS
The published primitives are narrow and mostly rely on either (a) a bug in the OS that lets the attacker modify the shadow stack, or (b) attack vectors that don’t flow through ret:
- Kernel write to the shadow stack. A kernel arbitrary write can clobber shadow-stack pages; the user-mode SSP will then match the attacker’s chosen return address.
WRSSenabled in the process. Some specialized processes (JIT engines, compatibility shims) enableWRSSso they can patch their own shadow stack. A bug that takes over such a process re-enables the classical ROP path.- Indirect-call-only chains. CET-SS only protects returns. A chain that uses only indirect calls is unaffected by CET-SS, but is still constrained by CFG (and IBT, if enabled).
SetThreadContextstyle primitives. Setting RIP viaSetThreadContextis policed bySetContextIpValidation(one of the bits above). Without that bit set, an attacker withTHREAD_SET_CONTEXTaccess can redirect a thread’sripdirectly without ever returning.- Data-only attacks. Corrupting program state without ever redirecting control flow. Same gap as on Linux: the CPU enforces what RIP looks like, not whether the program is doing the right thing.
CET IBT on Windows
IBT requires every indirect branch target to begin with ENDBR64 (the four-byte sequence f3 0f 1e fa, which is a NOP on pre-CET CPUs). The compiler emits ENDBR64 at the entry of every function whose address might be taken, plus at every case arm of any switch table that might be used by an indirect jump.
On Windows, /CETCOMPAT marks the image as compatible with CET shadow stacks; it does not by itself prove IBT enforcement. Forward-edge CET metadata and enforcement need to be audited separately from SHSTK compatibility. Whether IBT is actually enforced for the process depends on hardware support, compiler-emitted ENDBR64 landing pads, image metadata, and the user-mode CET policy bits for that process.
In 2026, CFG is still the dominant indirect-call defense in user-mode Windows. IBT is enabled in some kernel paths, in some Edge-component binaries, and in some hardened in-box utilities, but the broad ecosystem coverage that CFG enjoys is not yet there for IBT. Treat IBT on Windows as “increasingly common but not universal” and check on a per-binary basis.
The composition
A modern hardened user-mode Windows 11 binary will typically have all of these enabled:
- ASLR with high-entropy 64-bit randomization.
- DEP (
/NXCOMPAT). - CFG (
/guard:cf), often with EH continuation table validation (/guard:ehcont). - CET-SS strict (
/CETCOMPAT /CETCOMPATSTRICT). - CET-IBT, where supported.
- ACG (Arbitrary Code Guard), preventing any new RWX page allocation.
- /GS stack canaries.
- SafeSEH or
/guard:ehcontinuation metadata.
How they compose against the classic exploit-development playbook:
Attack step | Mitigation that breaks it | Required workaround
------------------------+--------------------------------+--------------------------
Predict module bases | ASLR / HEASLR | Memory disclosure leak
Run shellcode on stack | DEP | ROP / JOP / data-only
JOP via indirect call | CFG | Find valid CFG target,
| | or kernel/CFG-mgr bug
Classical ROP via ret | CET-SS strict | Kernel write to shadow
| | stack, or WRSS-enabled
| | target
Indirect branch to mid- | CET-IBT (where enforced) | Land only on ENDBR64
function gadget | | targets
Allocate RWX, copy code | ACG | Reuse existing exec
| | pages only
SetThreadContext->RIP | SetContextIpValidation | Avoid that primitiveThe practical net result for an attacker against a fully hardened binary: you usually need three primitives, not one.
- A memory disclosure (defeats ASLR; lets you compute addresses).
- A bug whose effect is data-only OR that lets you redirect indirect control flow to a valid CFG (and possibly xfg-matching) target whose semantics you can weaponize.
- Either a way to avoid
retentirely (call-only chains,SetThreadContextif uncatched) OR a primitive that writes to the shadow stack (typically a kernel bug, occasionally aWRSS-enabled process).
This is a meaningful jump in cost over the pre-CFG era, when (1) was sufficient.
Auditing a Windows binary’s mitigations
The two main auditing surfaces are the binary’s PE headers and the running process’s mitigation state.
dumpbin /headers and dumpbin /loadconfig
The PE optional header tells you ASLR, DEP, and the high-level CET-compat bit:
> dumpbin /headers C:\Windows\System32\notepad.exe | findstr /i "characteristics"
8160 DLL characteristics
High Entropy Virtual Addresses
Dynamic base
NX compatible
Guard
Terminal Server Aware
DLL characteristics ex
CET CompatibleKey flags in DLL characteristics:
Dynamic base→IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE→ ASLR.High Entropy Virtual Addresses→IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA→ 64-bit ASLR with full entropy.NX compatible→IMAGE_DLLCHARACTERISTICS_NX_COMPAT→ DEP.Guard→IMAGE_DLLCHARACTERISTICS_GUARD_CF→ CFG.
Key flags in DLL characteristics ex (the extended characteristics field, requires recent dumpbin):
CET Compatible→IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT→ set by/CETCOMPAT.CET Compatible Strict Mode→IMAGE_DLLCHARACTERISTICS_EX_CET_COMPAT_STRICT_MODE→ set by/CETCOMPATSTRICT.CET set context IP validation relaxedand friends fine-tune the policy.
Then dumpbin /loadconfig for the CFG-specific data:
> dumpbin /loadconfig C:\Windows\System32\notepad.exe | findstr /i "guard"
GuardCFCheckFunctionPointer = 1407FE3D8
GuardCFDispatchFunctionPointer = 1407FE3E0
GuardCFFunctionTable = 14001A000
GuardCFFunctionCount = 1A4
GuardFlags = 00010500
CF Instrumented
CF Function Table Present
Long jump targets present
GuardAddressTakenIatEntryTable = 0
GuardLongJumpTargetTable = 14001A410
GuardLongJumpTargetCount = 7A binary with CFG fully enabled will have CF Instrumented, CF Function Table Present, a non-zero GuardCFFunctionCount, and a non-null GuardCFDispatchFunctionPointer. Pre-CFG and opt-out binaries have these zeroed.
Get-ProcessMitigation (PowerShell)
For a running process or an image on disk:
PS> Get-ProcessMitigation -Name notepad.exe
ASLR:
ForceRelocateImages : ON
RequireInfo : OFF
BottomUp : ON
HighEntropy : ON
DEP:
Enable : ON
EmulateAtlThunks : OFF
ControlFlowGuard:
Enable : ON
StrictControlFlowGuard : OFF
UserShadowStack:
UserShadowStack : ON
UserShadowStackStrictMode : ON
AuditUserShadowStack : NOTSET
SetContextIpValidation : ON
SignedBinaries:
MicrosoftSignedOnly : OFF
AllowStoreSignedBinaries : OFF
EnforceModuleDependencySigning : OFF
PayloadRestrictions:
EnableExportAddressFilter : OFF
EnableExportAddressFilterPlus : OFF
EnableImportAddressFilter : OFF
EnableRopStackPivot : OFF
EnableRopCallerCheck : OFF
EnableRopSimExec : OFFUserShadowStack: ON and UserShadowStackStrictMode: ON together mean strict CET-SS. ControlFlowGuard: Enable: ON means CFG. StrictControlFlowGuard: ON (the policy bit, not the linker flag) means CFG enforcement extends to non-CFG-instrumented modules being blocked from loading.
You can apply system-wide defaults in Set-ProcessMitigation -System -Enable ... and per-process exceptions in Set-ProcessMitigation -Name <exe> -Enable .... The same surface backs the Windows Security UI under “Exploit protection settings.”
System Informer / Process Hacker mitigation tab
For interactive auditing, System Informer (the maintained fork of Process Hacker) exposes a Mitigations tab in process Properties → Security. It lists the same fields as Get-ProcessMitigation plus a few kernel-only bits, and shows the exact policy enforced for that running process (which can differ from the on-disk image bits if Set-ProcessMitigation -Name overrides are present).
A quick sanity loop
To confirm a binary really has the protections you expect, the standard one-liner combination is:
# Static (image bits):
dumpbin /headers $img | Select-String 'Dynamic base|NX compatible|Guard|CET'
dumpbin /loadconfig $img | Select-String 'GuardCFFunctionCount|GuardFlags'
# Runtime (process policy applied by the kernel):
Get-ProcessMitigation -Name (Split-Path -Leaf $img)
# Or for a live PID:
Get-ProcessMitigation -Id $pidAnything where the runtime view contradicts the image view is interesting: it usually means a per-process override is in effect (good or bad) or the loader silently downgraded a mitigation due to a non-compliant DLL.
Closing notes
CFG and CET-SS together do not make Windows 11 unexploitable. They turn the work of exploitation from “find one bug, redirect RIP, drop shellcode” into “find a bug, find a memory leak, find a CFG-compatible target whose semantics you can weaponize, and either avoid ret entirely or compromise the shadow stack.” The number of public exploits against fully-hardened Windows 11 user-mode targets is small precisely because that bar is now real. The class of attacks that survives is dominated by data-only corruption (type confusion, JIT hardening misses, sandbox escapes that exploit logic rather than control flow) and by kernel bugs that defeat the shadow stack from below.
Windows Kernel Pool Overflow Foundations with HEVD, earlier in this series, is the natural complement: it crosses the boundary into ring 0, where the mitigation picture is different again (kernel CET, KCFG, KASLR, SMEP, SMAP, HVCI, VBS), and where many of the techniques that defeat user-mode shadow stacks ultimately live. Each of those kernel-side mitigations owes much of its design to lessons learned from the user-mode mitigations covered here.