pk.org: Computer Security/Lecture Notes

Memory Vulnerabilities and Defenses -- Study Guide

Paul Krzyzanowski – 2025-10-20

Understanding memory vulnerabilities is essential to understanding how systems fail and how they are defended. We wans to understand how memory errors arise, how they are exploited, and how modern systems defend against them.

Memory Corruption and Exploitation

Most software vulnerabilities stem from incorrect handling of memory. In C and C++, the compiler trusts the programmer to manage memory correctly. When programs read or write beyond valid memory boundaries, they corrupt nearby data and sometimes control structures. These problems lead to memory corruption, the root cause of buffer overflows, integer overflows, and use-after-free bugs.

A buffer overflow happens when data exceeds the size of a destination buffer. On the stack, this may overwrite the saved return address. On the heap, it may overwrite allocator metadata or neighboring blocks. Both cases give an attacker the opportunity to redirect execution or corrupt important data.

Integer overflows are subtler. Arithmetic that exceeds the maximum value of a type wraps around to zero. A calculation that allocates too small a buffer, for example, can make a later copy operation overwrite adjacent memory. Off-by-one errors fall into the same category: a loop that copies one extra byte can overwrite a boundary value such as a null terminator or a saved pointer.

Use-after-free bugs occur when a program continues to use memory after freeing it. If the allocator reuses that memory for a different purpose, the program may read corrupted data or allow an attacker to control object contents.

Format-string vulnerabilities appear when untrusted input is used directly as a format argument to printf or similar functions. Directives such as %x print data from the stack, and %n writes a value to a memory address that is read from the stack. If the format string comes from user input, the attacker can read memory or even write arbitrary values to attacker-chosen locations.

Early exploits injected shellcode, machine instructions placed into a writable buffer, and redirected execution to run them. When systems began marking writable pages as non-executable, attackers adapted their techniques to work within these new constraints.

Defensive Mechanisms

Each defensive measure was developed to close a gap that earlier systems left open. Together, they form the layered protection that modern systems rely on.

Non-executable memory (NX, DEP, W^X)

The first step was to separate code from data. NX (No eXecute) or DEP (Data Execution Prevention) marks writable memory as non-executable. This capability is provided by the processor's memory management unit (MMU) and configured by the operating system when it sets up page permissions. The CPU refuses to execute any instructions from pages marked non-executable, preventing injected shellcode from running. NX does not stop memory corruption itself, but it eliminates the simplest outcome: running arbitrary injected code.

Adapting to non-executable memory

When NX made shellcode injection impractical, attackers shifted to code reuse techniques. These approaches work because they execute only code that is already marked executable: they simply chain it together in ways the original programmers never intended.

Return-to-libc was the first widely used code reuse technique. Instead of injecting shellcode, an attacker overwrites a return address to point to an existing library function such as system(). By carefully arranging data on the stack, the attacker can make that function execute with attacker-chosen arguments. For example, redirecting to system("/bin/sh") spawns a shell without injecting any code.

Return-to-libc works because library functions are already executable. The attack reuses trusted code for untrusted purposes. The main limitation is that the attacker must find a single function that accomplishes their goal and must be able to set up its arguments correctly.

Return-oriented programming (ROP) generalizes this idea. Instead of calling a single function, ROP chains together short sequences of instructions called gadgets. Each gadget is a fragment of existing code that ends with a return instruction. By placing a sequence of gadget addresses on the stack, an attacker can compose arbitrary computation from these fragments.

ROP works because each gadget ends with a return, which pops the next address from the stack and jumps there. The attacker controls what addresses are on the stack, effectively writing a program out of pre-existing instruction sequences. With enough gadgets, an attacker can perform any operation (load values, perform arithmetic, make system calls) all without injecting a single byte of code.

ROP is more powerful than return-to-libc but also more complex. The attacker must find suitable gadgets in the executable memory of the target process and must know their addresses. This requirement explains why address randomization (ASLR) is so important: it makes gadget locations unpredictable.

Address-space layout randomization (ASLR)

Return-to-libc and ROP showed that NX alone was not enough. Attackers could still call existing functions or chain gadgets if they knew their addresses. ASLR fixed that by randomizing the layout of the process address space. Each run places the stack, heap, and libraries at unpredictable locations. Without that knowledge, hardcoded addresses no longer work reliably. ASLR's strength depends on the randomness available and on the absence of information leaks that reveal memory addresses.

Stack canaries

Stack canaries add a small random value between local variables and saved control data on the stack. The program checks the value before returning from a function. If the canary changed, execution stops. This defense detects stack overflows that overwrite return addresses, preventing direct control hijacking. The idea is simple but powerful: any corruption that changes the control data must also change the canary.

Heap canaries and allocator hardening

Heap corruption exploits were once as common as stack overflows. Modern allocators introduced defenses modeled after stack canaries and added several more.

Heap blocks may include heap canaries (or cookies): small guard values placed before or after each block's user data. When a block is freed, the allocator verifies that the guard is intact. If an overflow or underflow modified it, the program aborts.

Allocators also use safe unlinking to validate free-list pointers, pointer mangling to encode metadata with a secret, and quarantining to delay reuse of freed blocks. These techniques make heap corruption much less predictable and far harder to exploit.

Safer libraries and compiler checks

Many vulnerabilities arise from unsafe standard functions such as gets, strcpy, or sprintf, which do not enforce buffer limits. Modern compilers and libraries address this by warning developers or substituting safer variants like fgets, strncpy, and snprintf. Options such as FORTIFY_SOURCE perform runtime checks to detect unsafe copies. The goal is to eliminate the easy mistakes that once led to catastrophic failures.

Linker and loader hardening

Dynamic linking once allowed attackers to tamper with relocation tables and redirect function calls. Linker and loader hardening, such as RELRO (RELocation Read-Only), marks these tables read-only after initialization and resolves symbols early. This removes the possibility of overwriting linkage data to redirect control flow.

Development-time Protections

Preventing memory vulnerabilities during development is more effective than mitigating them at runtime. Modern testing tools make many memory bugs visible before deployment.

Compiler instrumentation can add runtime checks to detect invalid memory operations and arithmetic errors during testing. These checks turn silent corruption into clear, reproducible failures. For example, a compiler can detect writes outside array bounds, uses of freed memory, or overflows in signed arithmetic. These features are used only during development because they slow execution, but they find the same classes of vulnerabilities that attackers exploit.

Fuzzing complements compiler instrumentation by generating a large number of random or mutated inputs, watching for crashes and test failures. Coverage-guided fuzzers automatically explore new code paths and expose edge cases that human testing might never reach. Fuzzing does not prove correctness;it simply finds the conditions that lead to failure. Combined with compiler instrumentation, it is one of the most effective ways to uncover memory-safety bugs before software is released.

Together, these testing tools address the visibility problem: they make hidden memory errors observable and fixable long before deployment.

Hardware Mechanisms

Modern processors now assist in enforcing memory safety directly.

Control-flow integrity (CFI). Hardware support such as Intel's Control-flow Enforcement Technology (CET) protects return addresses and indirect branches. A shadow stack stores a verified copy of each return address, detecting tampering. Indirect branch tracking ensures jumps go only to legitimate targets.

Pointer authentication. Some architectures add a short integrity check to pointer values so the processor can detect when a pointer has been modified. This prevents forged return addresses or corrupted function pointers from being used.

Memory tagging. Hardware can associate small tags with memory allocations and pointers. The processor checks the tags on each access, revealing use-after-free and out-of-bounds errors. These features extend the same principle as software defenses (detect corruption and verify integrity) but enforce it in hardware.

How the Layers Work Together

Memory protection is not one mechanism but a collaboration across the system.

Each layer covers weaknesses the others cannot. NX stops shellcode. ASLR hides addresses. Canaries detect overwrites. Allocator hardening prevents metadata abuse. Hardware features validate control flow. Testing tools find the bugs that remain. No single technique provides security, but together they make exploitation unreliable and expensive.

Core Takeaways