Level 2 · Chapter 12
Exploit mitigations (CFG, ACG, ASLR, CIG)
OS- and compiler-level defenses that assume a memory-corruption bug exists and try to stop it from becoming arbitrary code execution.
Every mechanism in this level so far has assumed the code actually running is the code that was shipped, and asked what that code is allowed to touch. Exploit mitigations are built on the opposite assumption: that somewhere in a large enough codebase, a memory-corruption bug exists and will eventually be found, and the right response is to make that bug far harder to turn into arbitrary code execution — without needing to know what the specific bug is in advance.
A sequence of narrowing hoops, not one single defense
It helps to think of a real exploit attempt as a chain: first the attacker needs to know where useful things are in memory, then they need a way to redirect execution somewhere useful, then whatever they redirect to has to actually run as code. Each mitigation here targets exactly one link in that chain, and defeating any single link breaks the whole chain for a huge share of real-world exploitation techniques — which is why these are deployed together rather than as alternatives to each other.
ASLR: making memory addresses impossible to hardcode
Address Space Layout Randomization randomizes where DLLs, the executable image, the stack, and heap allocations load on each run. An exploit that depends on jumping to a specific, known address — a function inside a loaded DLL, say, used as a "gadget" in a return-oriented-programming chain — simply doesn't work if that address is different every time the process starts. ASLR doesn't stop a bug from corrupting memory; it stops the attacker from reliably knowing where anything useful is once they have.
CFG: checking indirect calls against a map of legitimate targets
Control Flow Guard has the compiler emit a bitmap, built at compile time, of every address that's a legitimate target for an indirect call in that binary — function pointers, virtual method tables, callback addresses. At runtime, before an indirect call is actually taken, a check (ntdll!LdrpValidateUserCallTarget doing the real work) verifies the target address is marked valid in that bitmap. A classic exploitation step — corrupt a function pointer or vtable entry, then trigger a call through it to redirect execution to attacker-chosen code — stops working, because the corrupted target almost certainly isn't a bit CFG considers a legitimate call target.
ACG: making writable and executable mutually exclusive
Even with CFG, an attacker who can write arbitrary data into the process might still try the oldest trick available: write a small piece of machine code (shellcode) into some writable region and jump to it. Arbitrary Code Guard closes this off structurally: it enforces that no page in the process can be both writable and executable at the same time, and blocks remapping an existing executable page as writable. A process with ACG enabled genuinely cannot create a fresh page of injected, attacker-written code and then execute it — there's no sequence of allowed memory-protection transitions that gets there.
CIG: restricting what can load at all
ACG stops an exploit from running new, injected code, but a resourceful attacker can instead try to load an entire DLL of their own — one that isn't shellcode at all, just an ordinary, functioning binary that happens to do what they want. Code Integrity Guard closes that path too, by restricting the process to loading only images signed by Microsoft or another policy-defined, trusted signer. Between ACG and CIG together, a process can neither manufacture new executable code nor load someone else's.
These are opt-in, and posture varies process to process
None of this is a blanket, always-on OS behavior. A process's mitigation set is configured via SetProcessMitigationPolicy (or baked into the image's load configuration) and can be queried with GetProcessMitigationPolicy. High-risk processes — browser renderer sandboxes being the canonical example, since they parse untrusted content from the open internet by design — are typically locked down with the full set; an ordinary line-of-business desktop application frequently has few or none of these enabled, because older or less carefully audited code may legitimately need to do things (generate code at runtime, load unsigned plugins) that CIG or ACG would outright break.
A worked example: why a browser's renderer looks so restricted compared to its own browser chrome process
Modern browsers deliberately split into multiple processes, and the one parsing untrusted web page content typically runs with ACG and CIG both active, while the main browser-UI process does not need to be anywhere near as restricted. If an attacker finds and successfully exploits a memory-corruption bug in the page-rendering code, ACG stops them from writing and running fresh shellcode, and CIG stops them from loading an unsigned helper DLL as a workaround — both defenses apply regardless of what the specific rendering bug was, which is exactly the point: the mitigation doesn't need updating every time a new rendering bug is patched.
A common mistake
Treating any of these as making a process "unexploitable," or as interchangeable with each other, misunderstands what they actually guarantee. Each one closes off one specific technique; a sufficiently motivated exploit chain routinely combines several distinct bugs specifically to route around whichever single mitigation is in the way (for instance, finding an information-disclosure bug to defeat ASLR before using a separate corruption bug). None of these replace fixing the underlying memory-safety defect — they raise the cost and narrow the toolkit available to someone trying to exploit one that hasn't been found yet.
Where this connects
- Kernel Patch Protection (PatchGuard) & HyperGuard is the equivalent idea applied to the kernel itself, rather than to a user-mode process.
- The DLL loader, PEB, and module lists is the exact machinery CIG restricts and ASLR randomizes the layout of.
- Processes & threads covers the process-creation path where a mitigation policy actually gets attached to a new process.