Chapter 35 Key Takeaways: Buffer Overflows and Memory Corruption
-
A buffer overflow is C doing exactly what you told it to do: writing past the end of a buffer overwrites adjacent memory. The compiler inserts no bounds check. On the stack, that adjacent memory is the saved RBP and then the return address — the CPU will jump wherever the return address points when
retexecutes. -
The offset to the return address is deterministic: for a function with
char buf[N]and a standard x86-64 frame, the offset frombuf[0]to the return address isN + 8(N bytes of buffer plus 8 bytes of saved RBP). This can be confirmed with GDB's cyclic pattern tool. -
Shellcode must be position-independent and null-byte-free: position-independent because stack addresses are unpredictable (ASLR); null-byte-free because many overflow vectors use string functions that terminate at
\0. The classicexecve("/bin/sh")shellcode encodes the path by pushing it to the stack at runtime and usesmov al, 59instead ofmov rax, 59to avoid zero bytes in the immediate. -
NOP sleds were a reliability technique, not a fundamental component: a sequence of
NOP(0x90) instructions before shellcode allowed imprecise landing before ASLR made addresses unpredictable. Understanding NOPs as a defensive indicator (malware may still use them for historical-compatibility shellcode) is more relevant today than understanding them as an offensive technique. -
Format string vulnerabilities arise from passing user-controlled data as the format string:
printf(user_input)lets the attacker use%pto read stack contents and%nto write values to memory. The fix is alwaysprintf("%s", user_input). Modern compilers warn about this with-Wformat-security. -
Use-after-free is now the dominant heap exploitation technique: accessing memory through a pointer after it has been freed can be exploited when the attacker can control what gets allocated at the freed address. If a function pointer is at the freed location, controlling the new allocation contents means controlling execution.
-
Heap chunk headers in glibc contain allocator metadata:
prev_size,size, and (when freed)fd/bkfree list pointers. Overflowing into adjacent heap chunk headers corrupts these pointers, which are followed during subsequentmalloc/freeoperations, potentially allowing controlled writes. -
The Morris Worm (1988) established the template for network exploitation: buffer overflow in
fingerd'sgets()call → shellcode injection → shell execution → lateral movement. The response established CERT/CC, led togets()deprecation, and accelerated secure coding research. Every mitigation in Chapter 36 traces its lineage to exploits like this. -
Every dangerous C function has a safe alternative:
gets()→fgets();strcpy()→strlcpy();sprintf()→snprintf();scanf("%s")→scanf("%63s"). The safe versions require a size argument and respect it. -
AddressSanitizer (
-fsanitize=address) catches memory corruption at runtime: buffer overflows, use-after-free, double-free, and heap corruption are detected immediately with a clear report including the allocation and use locations. Use ASan in testing; it is essential for finding vulnerabilities before attackers do. -
The three conditions required for classic shellcode injection no longer hold on modern systems: executable stack (defeated by NX/DEP), predictable addresses (defeated by ASLR), and no canary detection (defeated by stack canaries). Modern exploitation requires either bypassing multiple mitigations or using techniques that do not inject code (ROP, Chapter 37).
-
Double-free corrupts allocator metadata in glibc: freeing the same pointer twice puts a chunk on the free list twice, corrupting the
fdfield. Modern glibc has double-free detection, but custom allocators often do not. -
Heap spraying and grooming are techniques for controlling heap layout: allocating many objects of a specific size to force the allocator to return predictable addresses, or arranging allocations so that a freed object will be reallocated adjacent to an attacker-controlled buffer. These make UAF exploitation reliable.
-
Memory-safe languages prevent these vulnerabilities by design: Rust's borrow checker prevents use-after-free at compile time; bounds checks in Java, Go, Python, and C++ STL containers prevent buffer overflows at runtime. Choosing the right tool for the job includes considering the security properties of the memory model.
-
Defense in depth applies to memory corruption: no single mitigation is sufficient. Stack canaries catch overwrites; NX prevents shellcode; ASLR makes addresses hard to predict; AddressSanitizer catches bugs in testing; static analysis catches dangerous patterns in code review; memory-safe languages prevent whole classes of bugs. Together these layers make exploitation very difficult even when vulnerabilities exist.