Stack buffer overflow

Stack buffer overflow

TL;DR: Write past a fixed-size stack array into the saved return address (or saved EBP/RBP, or an SEH record) and on function return the CPU jumps wherever the attacker wrote.

What it is

The stack stores per-call frames: locals, saved frame pointer, saved return address (and on 32-bit Windows, also the SEH chain). A function copying attacker-controlled input into a local buffer without bounds-checking — strcpy, gets, unchecked memcpy(local, src, attacker_len), vulnerable sprintf — extends past the buffer into adjacent saved state. When the function executes ret, it pops the corrupted address into RIP/EIP and the attacker controls execution.

Preconditions / where it applies

  • A function with a fixed-size stack buffer and unbounded input copy
  • Either no stack cookie, or a leak / bypass for it
  • Either DEP off and a known stack address (legacy), or DEP on with dep-bypass via rop-chains
  • Either ASLR off, or a leak (see aslr-bypass)

Technique

Classical workflow:

  1. Trigger and crash. Send a long pattern (pattern_create.rb 5000 or cyclic 5000) — observe RIP/EIP in WinDbg.
  2. Find offset. pattern_offset or cyclic_find against the crashed RIP value → exact distance from start of input to saved return address.
  3. Enumerate bad characters. Send \x01\x02..\xff and see which bytes get stripped/transformed by the receive path — see bad-character-handling.
  4. Locate a stable trampoline. Without ASLR/DEP: jmp esp / call esp gadget in a loaded module via !mona jmp -r esp.
  5. Build the payload. [ pad to offset ] + [ trampoline addr ] + [ NOP sled ] + [ shellcode ].
  6. With DEP+ASLR. Replace step 5 with a ROP chain to VirtualProtect (see rop-chains, dep-bypass). Need a non-ASLR module or info-leak.

Minimal POC (32-bit, DEP/ASLR off):

1
2
3
4
5
6
7
8
9
import socket, struct
offset = 1024
ret    = struct.pack('<I', 0x10101010)  # jmp esp in some module
nops   = b'\x90' * 16
shell  = b'\xcc' * 4                    # int3 placeholder
payload = b'A'*offset + ret + nops + shell

s = socket.create_connection(('target', 9999))
s.sendall(b'OVERFLOW ' + payload + b'\r\n')

When the buffer is small, drop an egghunter and stash the real payload elsewhere. When you can only overwrite SEH, see seh-overwrite.

Detection and defence

  • /GS stack cookie (Visual Studio) — random canary placed before saved EBP/return; __security_check_cookie aborts on mismatch
  • DEP / NX makes shellcode-on-stack non-executable
  • ASLR randomises module bases — no static jmp esp
  • CFG (control-flow-guard) + CET shadow stack (cet-shadow-stack) break ret-based and indirect-call gadget reuse
  • Compile with /GS /guard:cf /CETCOMPAT /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT; statically replace dangerous APIs with bounded variants (strncpy_s, StringCchCopy)

References

Related: seh-overwrite, rop-chains, dep-bypass, mona-py