Chapter 201

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.

Scenario

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)

x64 PROCESS VIRTUAL ADDRESS SPACE (Windows) ═══════════════════════════════════════════════════════════════════════ 0x0000000000000000 ← NULL page (unmapped, catches null deref) 0x0000000000010000 ← lowest user allocation ... User space (~128 TB): Stack grows downward from ~0x00007FF000000000 Heap grows upward from above code/data segments Loaded DLLs scattered across user range (ASLR randomizes bases) Code (.text) RX, Data (.data/.bss) RW, Read-only (.rdata) R ... 0x00007FFFFFFFFFFF ← top of user range ────────────────── (kernel/user boundary) 0xFFFF800000000000 ← kernel range (inaccessible from user mode) KEY REGISTERS (x64 calling convention): RIP Instruction pointer (return address on stack) RSP Stack pointer (points to top of stack) RBP Frame pointer (base of current stack frame) RCX/RDX/R8/R9 First four integer arguments (Windows ABI) XMM0-XMM3 First four floating-point arguments STACK FRAME LAYOUT (function call): ┌────────────────────────────┐ ← high address │ caller's stack frame │ ├────────────────────────────┤ │ return address (8 bytes) │ ← saved RIP; overwrite = control flow hijack ├────────────────────────────┤ │ saved RBP (8 bytes) │ ├────────────────────────────┤ │ local variables / buffers │ └────────────────────────────┘ ← RSP (low address; stack grows downward) ═══════════════════════════════════════════════════════════════════════

Exploit Mitigations

MitigationWhat it protectsBypass approachOS default (Win11)
ASLRRandomizes base addresses of image, stack, heapInfo leak → leak a pointer → compute baseOn (kernel enforced)
DEP/NX (Data Execution Prevention)Marks data pages non-executableROP — reuse existing executable code gadgetsOn
Stack Canary (GS cookie)Detects stack buffer overflow before returnLeak canary value OR bypass via non-sequential overwriteOn (MSVC /GS)
CFG (Control Flow Guard)Validates indirect call targets before dispatchROP + valid CFG target → call allowed function with controlled argsOn (opt-in per image)
CET (Control-flow Enforcement)Shadow stack validates return addressesMust corrupt shadow stack (requires separate primitive) or use JOPOn (new hardware + Win11)
HVCIPrevents unsigned kernel code executionSigned vulnerable driver (BYOVD) or hypervisor exploitOn (modern hardware)
Safe SEH / SEHOPValidates SEH chain before dispatchOverwrite with valid registered handler addressOn

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

EXPLOIT PRIMITIVE LADDER ═══════════════════════════════════════════════════════════════════════ Starting condition → Desired capability WEAK PRIMITIVES (stepping stones): ┌─────────────────────────────────────────────────────────────────┐ │ Out-of-bounds read → information disclosure (leak pointers) │ │ Heap metadata corrupt → arbitrary write primitive (write-what- │ │ where: write controlled value to │ │ controlled address) │ │ Stack OOB write → overwrite return address (control RIP) │ └─────────────────────────────────────────────────────────────────┘ STRONG PRIMITIVES (exploitation complete): ┌─────────────────────────────────────────────────────────────────┐ │ Arbitrary read → leak ASLR base addresses │ │ Arbitrary write → overwrite function pointer / RIP │ │ Code execution (RIP) → ROP chain → shellcode or syscall │ └─────────────────────────────────────────────────────────────────┘ MITIGATION BYPASS CHAIN (typical modern exploit): bug → OOB read (leak canary + image base) → OOB write (overwrite return address with ROP gadget) → ROP chain (disable NX or call VirtualProtect/mprotect) → shellcode execution ═══════════════════════════════════════════════════════════════════════

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.