Exploit Development Fundamentals
Exploit development transforms a vulnerability — a software defect that causes unintended behavior — into a controlled capability: arbitrary code execution, privilege escalation, or information disclosure. Modern exploit development must contend with a full stack of mitigations (ASLR, DEP/NX, stack canaries, CFG, CET) that individually raise the bar and collectively require multi-stage bypass chains. This chapter establishes the mental model: memory layout, vulnerability primitives, and the mitigation landscape that every subsequent exploit chapter builds on.
You are performing a security assessment of a legacy internal web application compiled for Windows x64 with ASLR and DEP enabled but no stack canaries and no CFG. The application parses user-supplied XML and uses a custom memory allocator. You need to understand the full exploit development pipeline — from identifying the bug class through to crafting a working exploit payload — before writing a single line of shellcode.
Process Memory Model (x64)
Exploit Mitigations
| Mitigation | What it protects | Bypass approach | OS default (Win11) |
|---|---|---|---|
| ASLR | Randomizes base addresses of image, stack, heap | Info leak → leak a pointer → compute base | On (kernel enforced) |
| DEP/NX (Data Execution Prevention) | Marks data pages non-executable | ROP — reuse existing executable code gadgets | On |
| Stack Canary (GS cookie) | Detects stack buffer overflow before return | Leak canary value OR bypass via non-sequential overwrite | On (MSVC /GS) |
| CFG (Control Flow Guard) | Validates indirect call targets before dispatch | ROP + valid CFG target → call allowed function with controlled args | On (opt-in per image) |
| CET (Control-flow Enforcement) | Shadow stack validates return addresses | Must corrupt shadow stack (requires separate primitive) or use JOP | On (new hardware + Win11) |
| HVCI | Prevents unsigned kernel code execution | Signed vulnerable driver (BYOVD) or hypervisor exploit | On (modern hardware) |
| Safe SEH / SEHOP | Validates SEH chain before dispatch | Overwrite with valid registered handler address | On |
Vulnerability Classes
// STACK BUFFER OVERFLOW — write past end of stack-allocated buffer
void vuln_stack(const char* input) {
char buf[64];
strcpy(buf, input); // no bounds check → overflow if input > 64 bytes
// Overwrites: local vars → saved RBP → saved RIP → caller's stack
}
// HEAP BUFFER OVERFLOW — write past end of heap allocation
void vuln_heap(int size, const char* data) {
char* buf = (char*)malloc(size);
memcpy(buf, data, strlen(data)); // strlen(data) may exceed size
// Overwrites adjacent heap chunk headers → heap metadata corruption
}
// USE-AFTER-FREE — access memory after it has been freed
struct Obj { void(*vfptr)(); int data; };
void vuln_uaf() {
Obj* o = (Obj*)malloc(sizeof(Obj));
o->vfptr = legit_function;
free(o);
// Attacker allocates same-size chunk → fills with controlled data including fake vfptr
o->vfptr(); // calls attacker-controlled address if reallocated with attacker data
}
// TYPE CONFUSION — cast object to wrong type; access fields at wrong offsets
// INTEGER OVERFLOW — arithmetic wraps, causing undersized allocation
// malloc(user_count * sizeof(record)) where user_count * size overflows DWORD
// FORMAT STRING — printf(user_input) → %n writes to arbitrary address
Fuzzing and Vulnerability Discovery
// Fuzzing: feed malformed/mutated input to target; monitor for crashes.
// Crash → potential exploitable condition (especially AccessViolation).
// Tools: WinAFL (coverage-guided), libFuzzer (LLVM sanitizers), boofuzz (network).
// Minimal WinAFL harness for a file parser:
// Compile target with AFL instrumentation; run WinAFL to mutate input files.
// Crash triage: !exploitable (WinDbg extension) categorizes:
// EXPLOITABLE: corrupted stack or heap pointer → execution redirected
// PROBABLY EXPLOITABLE: controlled data near corruption
// UNKNOWN: crash but no obvious control
// NOT EXPLOITABLE: null deref or assertion in trusted code
// AddressSanitizer (ASAN) in MSVC /fsanitize=address:
// Instruments every memory access; reports heap/stack OOB, UAF, double-free.
// Essential for finding exploitable bugs during fuzz corpus analysis.
// Cyclic pattern for offset discovery (e.g., pwntools cyclic()):
// Send AAAABBBBCCCCDDDD... as input
// On crash, RSP contains part of the pattern → offset = cyclic_find(rsp_value)
Exploit Primitive Hierarchy
Detection Engineering
title: Process Crash with Suspicious Module Context (Potential Exploitation)
logsource:
product: windows
service: application
detection:
selection:
EventID: 1000 # Application Error (WER)
ExceptionCode: '0xc0000005' # Access Violation
suspicious_offset:
FaultingModuleOffset|startswith: '0x00000000000041' # 0x41414141 pattern
condition: selection or suspicious_offset
level: high
tags: [attack.initial_access, T1203]
title: WER Fault Bucket — Repeated Crashes of Same Process (Fuzz/Exploit Attempt)
logsource:
product: windows
service: application
detection:
selection:
EventID: 1001 # WER Fault Bucket
timeframe: 5m
condition: selection | count() by EventData.AppName > 5
level: medium
-- MDE KQL: application crashes with access violation (potential exploitation)
DeviceEvents
| where ActionType == "ExploitGuardExploitDetected"
or ActionType == "ExploitGuardNetworkProtectionBlocked"
| project Timestamp, DeviceName, InitiatingProcessFileName,
AdditionalFields
-- MDE KQL: WER crash dump creation (may indicate exploit attempt)
DeviceFileEvents
| where Timestamp > ago(1d)
| where FolderPath has @"\CrashDumps\" or FolderPath has @"\WER\"
| where FileName endswith ".dmp"
| where InitiatingProcessFileName =~ "WerFault.exe"
| summarize crash_count = count()
by DeviceName, bin(Timestamp, 1h)
| where crash_count > 5
| order by crash_count desc
Q&A
ASLR randomizes base addresses but an exploit can still work if it leaks a single pointer. Why does one leaked pointer typically defeat the entire ASLR protection, and what design decisions make this possible?
ASLR randomizes the base address of each memory region (image, heap, stack) at load time. Within a given process instance, the base is fixed for the entire lifetime of that process. If an attacker can read a single pointer from that process's memory — any pointer into a loaded module, any stack address, any heap address — they learn the randomized base for that region. From that base, all other addresses within the same region are deterministic offsets that the attacker can compute from the binary's static layout.
For a DLL like ntdll.dll: if an attacker reads any pointer that points into ntdll's address range (e.g., a function pointer stored on the heap, a return address on the stack), they learn the loaded base of ntdll. The RVA (relative virtual address) of every function, gadget, and data structure within ntdll is identical to the static file layout — the attacker computes leaked_ptr - known_ntdll_rva + desired_rva to find any function or ROP gadget in ntdll. The entire module's address space is revealed by one leaked pointer.
The design decisions that make one leak sufficient: (1) Deterministic internal layout — ASLR only randomizes the base, not the internal structure of a loaded image. Within an image, offsets are fixed at compile time. (2) Image-level granularity — all pages of a given image share the same randomly-chosen base. A finer-grained scheme like ASLP (Address Space Layout Permutation) that randomizes per-function would require leaking one pointer per function. (3) Information reuse — a leaked stack pointer reveals the stack's randomized base and, via the call chain, return addresses into loaded images, leaking those images' bases simultaneously. A single read of the stack can leak multiple region bases in one shot. This is why modern mitigations like CET (shadow stack protecting return addresses) and pointer authentication (ARM PAC) focus on preventing the pointer-reuse step: even if you know the address of a return address, CET's shadow stack means overwriting the stack copy doesn't redirect execution because the shadow stack copy is still intact.