When a program crashes, the developer files a bug report. When an attacker causes the same crash, they call it a failed attempt.

What attackers really want is not a crash, it is controlled corruption. Memory modified in a precise, predictable way. A bug turned into a tool. Tools chained into a full attack.

This post explains three of the most common techniques attackers use to exploit memory corruption bugs, in plain terms, with examples.


Stack Buffer Overflow: Overwriting the Return Address

Think of the call stack as a notepad. When function A calls function B, the CPU writes a note on the stack: “when B is done, come back here.” That note is called the return address.

A stack buffer is just a fixed-size block of memory allocated on that notepad. If you write more data into the buffer than it can hold, the extra bytes spill over into adjacent memory, including, critically, the return address.

void vulnerable(char *input) {
    char buf[64];          // 64-byte buffer on the stack
    strcpy(buf, input);    // copies input into buf - no size check
}

If input is 100 bytes long, strcpy happily writes all 100 bytes starting at buf. The first 64 go into buf. The remaining 36 bytes overwrite whatever comes next on the stack, which includes the return address.

If the attacker controls those 36 bytes, they control where the CPU jumps when vulnerable returns. That is code execution.

What about modern defenses?

Stack canaries place a random value between the buffer and the return address. If the overflow overwrites the canary, the program detects the corruption and terminates. Effective, but only for overflows that touch the canary. Heap overflows bypass it entirely.

Non-executable memory (NX/DEP) prevents the CPU from executing code placed in the buffer. The attacker cannot put shellcode in buf and jump to it. This killed the classic attack, but not exploitation itself.

Return-Oriented Programming (ROP) is the modern answer. Instead of injecting new code, the attacker finds tiny snippets of existing code, a few instructions followed by a ret, scattered through the binary. By chaining these snippets together on the stack, the attacker constructs arbitrary computation without ever writing new code. NX is irrelevant: the code being executed was already there.

ASLR randomizes the memory layout so the attacker cannot predict where those snippets are. The counter: a separate bug that leaks a memory address can reveal the layout. Once one address is known, the entire layout is computable.

Modern exploits combine: a memory leak to break ASLR, an overflow to overwrite the return address, and a ROP chain to execute the payload.


Use-After-Free: Poisoning Freed Memory

Imagine you check out of a hotel room. The hotel marks it as available. A new guest checks in and starts putting their belongings in the room. Meanwhile, you kept a copy of the room key and walk back in.

That is use-after-free (UAF). The program frees a chunk of memory, but keeps a pointer to it. The allocator reuses that memory for something else. The stale pointer still thinks the old object is there, but now it is pointing at someone else’s data.

class Button {
public:
    virtual void onClick() { /* handle click */ }
};

Button *btn = new Button();
delete btn;                   // btn is freed - memory returned to allocator

// attacker triggers an allocation that reuses btn's memory slot
// attacker fills that slot with controlled data

btn->onClick();               // UAF: CPU reads vptr from freed memory

Why is the vtable important? In C++, every class with virtual methods has a virtual function table (vtable), a list of function pointers. Each object stores a pointer to its class’s vtable (the vptr). When you call a virtual method, the CPU reads the vptr, finds the right function pointer in the table, and jumps to it.

If the attacker fills the freed memory slot with a fake vtable pointing to attacker-controlled code, btn->onClick() jumps there instead. That is code execution.

This technique drove browser exploitation for years. JavaScript engines allocate and free C++ objects in response to JavaScript execution, giving the attacker precise control over heap timing. CVE-2021-30563 and CVE-2022-1096 (both Chrome V8) are real-world examples of this exact pattern.


Type Confusion: Reading Memory As the Wrong Type

Every object in memory is just bytes. What makes those bytes meaningful is the type, the interpretation the program applies to them.

Type confusion occurs when the program reads an object as a different type than what it actually is. Fields are read at the wrong offsets. Values make no sense. And an attacker who can trigger this confusion can sometimes read or write memory they should not be able to touch.

class Dog {
public:
    int legs;
    char *name;    // pointer to a string - stored at offset 4
};

class Snake {
public:
    int legs;
    int length;    // an integer - also at offset 4, but not a pointer
};

If the program allocates a Snake but accidentally reads it as a Dog, it will read length (an integer) as name (a pointer). It will then try to dereference that “pointer”, jumping to whatever memory address the integer happens to represent.

In JavaScript engines, this is particularly powerful. JavaScript values are typed at runtime. A confusion between a JavaScript object and a typed array can give the attacker raw memory access, the ability to read and write arbitrary bytes within the process. CVE-2021-30551 (Chrome V8) achieved exactly this, leading to full remote code execution.


How Attackers Chain These Together

No single primitive gives immediate code execution in a modern, hardened target. Real exploits chain multiple steps:

Step 1 — Leak a memory address (defeats ASLR): an out-of-bounds read or uninitialized memory read exposes an address from the process’s memory layout.

Step 2 — Corrupt memory (write to a controlled address): a buffer overflow, UAF, or type confusion provides the ability to write attacker-controlled data somewhere useful.

Step 3 — Hijack control flow: the write is used to overwrite a function pointer, vtable pointer, or return address.

Step 4 — Execute a payload: a ROP chain achieves arbitrary code execution within the process.

Step 5 — Escalate or escape: a second vulnerability, often a kernel bug, elevates privileges or breaks out of a browser sandbox.

Each step uses a specific primitive. The attacker’s job is to find bugs that provide each primitive, then assemble the chain.


The Defender’s Asymmetry

The attacker needs one working chain. The defender needs to eliminate every link.

Mitigations raise the cost, but sufficiently motivated attackers find paths around each one. Formal verification addresses the problem differently: it proves the underlying bugs cannot exist. No out-of-bounds read means no address leak. No use-after-free means no write primitive. No primitive means no chain.