Skip to content
pwnsy
exploitationintermediate#buffer-overflow#memory-safety#exploitation#binary-security#cwe

Buffer Overflow Explained: Overwriting the Return Address

How stack buffer overflows work, why writing past a buffer overwrites the return address, and the mitigations that stop hijacked control flow.

A buffer is a block of memory set aside to hold a known amount of data: a 64-byte scratch area for a username, a fixed array for a line of input. A buffer overflow is what happens when a program writes past the end of that block. The extra bytes do not vanish. They overwrite whatever memory happens to sit next in line, and on the stack, that neighbor is frequently something the program relies on to keep running correctly.

This is one of the oldest bug classes in software, and it is still one of the most studied. Understanding it is the foundation for understanding almost everything in memory-corruption security. MITRE tracks the general case as CWE-787: Out-of-bounds Write and the stack-specific form as CWE-121: Stack-based Buffer Overflow, and out-of-bounds writes sit near the top of the most dangerous weakness lists year after year.

How a function uses the stack

When a program calls a function, it sets up a small region of memory called a stack frame. That frame holds the function's local variables, including any fixed-size buffers, along with bookkeeping the processor needs. One critical piece of bookkeeping is the return address: the location in the calling function that execution should jump back to once this function finishes.

The layout matters. On common architectures the stack grows in one direction while buffer writes grow in the other, so a local buffer and the saved return address sit close together in a predictable order. A write that runs off the end of the buffer moves toward the saved return address.

Turning an overflow into hijacked control

Consider a function that copies attacker-influenced input into a fixed local buffer without checking the length. If the input is longer than the buffer, the copy keeps writing past the buffer's end. Given enough length, it reaches and overwrites the saved return address.

Now the return address no longer points where the compiler intended. When the function finishes and the processor reads that address to decide where to continue, it jumps to a location the attacker chose through the contents of their oversized input. Control flow has been hijacked. Where it goes next depends on what else the attacker arranged in memory, which is the subject of guides on shellcode and exploit mitigations.

The key insight is that the bug itself is mundane: a copy that did not check a length. The consequence is severe because of what sits next to the buffer.

Why C and C++ dominate this bug class

Languages like C and C++ let code write to raw memory addresses and do not check array bounds at runtime by default. That control is why they are used for operating systems and browsers, and it is also why the same buffer overflow that a memory-safe language would catch as an error becomes a silent out-of-bounds write here. The bug class is a direct consequence of the language model.

A conceptual walkthrough

Picture a function that reads a line of input into a local buffer of 64 bytes. In its stack frame, the buffer sits at a known offset, and above it, in the direction the copy grows, live the saved frame pointer and the saved return address. The compiler laid them out in that order, and the program depends on the return address being intact when the function ends.

Now feed the function 200 bytes. The copy has no length check, so it keeps writing past byte 64. It fills the rest of the buffer, runs over the saved frame pointer, and reaches the saved return address, overwriting all of it with attacker-supplied bytes. Nothing crashes yet, because the function is still running and has not looked at the return address.

The moment of failure comes at the return. The processor reads the saved return address to decide where to continue, and it finds the attacker's bytes instead of the compiler's value. It jumps there. If the attacker filled that slot with an address that points at code of their choosing, execution continues under their control. If they filled it with garbage, the program jumps somewhere invalid and crashes, which is why so many overflows first surface as mysterious crashes before anyone realises they are exploitable.

The whole event turns on offsets and order. The attacker needs to know how many bytes lie between the start of the buffer and the saved return address, and what value to place there. The bug that made it possible was one unchecked copy. Everything dramatic that follows is a consequence of what the language allowed that copy to do.

An overflow does not have to reach the return address to be dangerous

Overwriting the return address is the textbook path, and it is only one outcome. A shorter overflow can corrupt other local variables, a saved frame pointer, a function pointer, or a flag that controls a security decision, changing the program's behaviour without ever redirecting the return. Any write past a buffer's end is a defect, and the return-address case is simply the most direct route to code execution.

Why bounds checking is the real fix

Every mitigation below is valuable, but they all sit downstream of the actual defect. The defect is a write whose length was never checked against the size of its destination. If code copies n bytes into a buffer that holds m, and n can exceed m, the program is one long input away from an overflow.

Functions that copy or concatenate without a length limit are the classic offenders. Their bounded replacements take a maximum length and stop there. Even the bounded versions require care, because an off-by-one error or a miscalculated size reintroduces the same problem. The discipline is simple to state: for every write into a buffer, know the buffer's size and refuse to write past it.

The mitigation stack

Because unsafe code exists in enormous quantities and cannot be rewritten overnight, the industry built layers that make a given overflow harder to exploit even when the bug is present. Each layer targets a different step of the attack.

MitigationWhat it doesWhat it forces the attacker to do
Stack canaryPlaces a secret value between local buffers and the return address, checked before returnLeak or avoid overwriting the canary
Non-executable memory (DEP/NX)Marks the stack and heap as non-executableReuse existing executable code instead of injecting new code
ASLRRandomizes where code and data load in memoryLeak an address first to know where to jump
Control-flow integrityRestricts indirect jumps to valid targetsFind gadgets that satisfy the policy
Memory-safe languageBounds-checks or borrow-checks at the language levelThe bug class does not compile or is caught at runtime

Stack canaries

A stack canary is a random value the compiler places just before the saved return address when the function starts. Before the function returns, generated code checks that the canary is unchanged. An overflow large enough to reach the return address must pass through the canary first, corrupting it. The check fails, and the program aborts instead of jumping to the attacker's address. Canaries do not stop the overwrite, they detect it in time to refuse the return.

Non-executable memory and ASLR

Historically an attacker would inject their own machine code into the buffer and point the return address at it. Marking the stack non-executable (DEP on Windows, NX elsewhere) breaks that plan: the injected bytes are data, and the processor refuses to run them. Attackers responded by reusing code already present in the program, a technique called return-oriented programming, covered in ASLR and DEP explained. Address space layout randomization then made even reuse hard, because the attacker no longer knows where the reusable code lives without first leaking an address.

None of these prevent the overflow. They raise the cost of converting it into reliable code execution.

How exploitation evolved

The history of this bug class is a back-and-forth, and knowing the sequence explains why the mitigation stack looks the way it does.

In the earliest era, an attacker put machine code directly into the overflowed buffer and pointed the return address at it. The stack was executable, so the injected bytes ran. This was simple and reliable, and it is the classic form the bug is taught in.

Non-executable memory ended that. With the stack marked non-executable, the injected bytes were data the processor refused to run. Attackers answered by reusing code already present and executable in the program. First came returning into existing library functions, then the more general return-oriented programming, which chains together short snippets already in the binary, each ending in a return, to build arbitrary behaviour out of the program's own instructions.

Address space layout randomization raised the bar again. Return-oriented programming needs to know where the reusable code lives, and randomizing load addresses hides that. Attackers responded by chaining an information leak, a separate bug or a side effect that discloses an address, with the overflow, using the leak to defeat randomization before redirecting control.

Control-flow integrity is the current layer, restricting indirect jumps and returns to legitimate targets so that arbitrary gadget chains no longer satisfy the rules. Research continues on both sides. The pattern across the whole history is consistent: each mitigation removes one easy path, the attacker finds a more expensive one, and the bar for a working exploit rises. None of it removes the underlying defect, which is why bounding the write at the source remains the real fix.

Buffer overflow is one member of a family of memory-safety defects, and placing it among its relatives sharpens what is specific to it.

Bug classWhere it happensTypical consequenceCWE
Stack buffer overflowLocal buffer on the stackOverwrites saved return address or localsCWE-121
Heap buffer overflowBuffer allocated on the heapCorrupts adjacent heap metadata or objectsCWE-122
Out-of-bounds write (general)Any buffer, any regionCorrupts whatever is adjacentCWE-787
Out-of-bounds readAny buffer being readDiscloses adjacent memory, aids info leaksCWE-125
Use-after-freeFreed heap allocation reusedAttacker controls a dangling objectCWE-416

The stack case is the most direct route to hijacked control because the return address sits so close to local buffers. The heap variants, covered in stack vs heap overflow, corrupt allocator metadata or neighbouring objects and usually need more steps to reach code execution. Out-of-bounds reads matter here too, because the information leaks that defeat ASLR are frequently read-side bugs. The whole family shares one root: a memory access that was never checked against the bounds of what it was allowed to touch.

How to defend against buffer overflows

The controls that actually reduce risk combine prevention at the source with the mitigation layers above.

  1. Bound every copy. Use length-limited functions and pass the true destination size. Treat any unbounded copy or concatenation into a fixed buffer as a defect in review.
  2. Validate input length before you act on it. Reject or safely truncate input that exceeds the size your buffers can hold, as close to the entry point as possible.
  3. Enable every compiler and linker mitigation. Turn on stack protection, non-executable memory, position-independent code with ASLR, and control-flow integrity where the toolchain supports it. These are usually flags, not rewrites.
  4. Prefer memory-safe languages for new code. For components that do not need manual memory management, a language that bounds-checks by default removes the class outright.
  5. Fuzz the parsers. Feed oversized and malformed input to any code that reads external data. Fuzzing finds the exact off-by-one and unchecked-length paths that manual review misses.
  6. Isolate the risky code. Run parsers and decoders with least privilege and in sandboxes, so an overflow that does get exploited lands in a constrained process.
Mitigations are a tax, not a wall

Every mitigation on its own has a known bypass in the research literature. Their value is cumulative: a canary plus non-executable memory plus ASLR plus control-flow integrity means an attacker needs a chain of primitives (often an information leak plus a write) rather than a single oversized input. Enable all of them, then still fix the underlying bug.

Signals that a crash is really an overflow

Many overflows announce themselves first as crashes, and triage decides which crashes deserve alarm. A few indicators separate an ordinary fault from a memory-corruption bug worth treating as a potential vulnerability.

  • A corrupted return address at the point of failure. If the program faults trying to execute at an address made of input bytes, such as a value that spells out part of the supplied data, the return address was overwritten. This is the clearest sign of a classic stack overflow.
  • A canary-failure abort. A program that terminates with a stack-protection message has detected an overwrite that reached the canary. The abort is the mitigation working, and it confirms a write ran past a buffer.
  • Crashes that scale with input length. If growing the input eventually triggers a fault at a predictable size, the input is overrunning a fixed buffer. The threshold hints at the buffer's size and the distance to whatever it corrupts.
  • Faults inside copy or parsing routines. Crashes in code that copies, concatenates, or parses external data point at the unchecked-length paths where these bugs live.
  • Sanitizer reports. Building with an address sanitizer turns a silent out-of-bounds write into an immediate, detailed report naming the buffer and the overrun, which is why fuzzing runs against sanitized builds.

The practical workflow is to reproduce the crash, rebuild the target with a sanitizer, and rerun the input. A sanitizer converts the vague crash into a precise account of which write went out of bounds and by how much, which turns a mysterious fault into a specific bug to fix.

Common misconceptions

"Stack canaries stop buffer overflows." A canary detects an overwrite that crossed it, and it does so at return time, after the overflow has already happened. It aborts the program to prevent a hijacked return. Overflows that corrupt data before the canary, or that leak the canary first, are not stopped by it. It is a detector, not a prevention.

"Non-executable memory makes the bug harmless." It blocks injecting and running new code on the stack, and it does nothing about reusing existing code through return-oriented programming. Attackers moved to code reuse precisely because of this mitigation. The overflow still happens and can still lead to execution.

"ASLR means addresses are unknowable." Randomization raises the cost of knowing where code lives, and an information leak elsewhere in the program can disclose an address and defeat it. ASLR is defeated routinely when paired with a leak, which is why it is one layer among several rather than a wall.

"Only huge overflows matter." A single byte past a buffer can corrupt a length field, a flag, or a pointer with serious consequences. Off-by-one errors are a whole sub-category of this bug class. Severity depends on what sits adjacent, not on how many bytes spilled.

"Rewriting in a memory-safe language guarantees safety." It removes this class from the safe portions of the code. Unsafe blocks, foreign-function calls into C libraries, and logic errors remain possible. Memory-safe languages are a large step, and they are not a blanket exemption from every memory bug.

Frequently asked questions

What is a buffer overflow in simple terms?

It is what happens when a program writes more data into a fixed-size block of memory than the block can hold. The surplus bytes overwrite whatever sits next to the buffer. On the stack, that neighbour is often the saved return address, so an overflow can redirect where the program goes when the current function finishes.

Why does overwriting the return address let an attacker run code?

When a function ends, the processor reads the saved return address to know where to continue. If an overflow replaced that address with a value the attacker controls, the processor jumps to a location of the attacker's choosing. Combined with content the attacker placed in memory, that redirection is what turns a memory bug into code execution.

What is the difference between a stack and a heap overflow?

A stack overflow overruns a buffer among a function's local variables, where the saved return address sits close by, giving a direct path to control-flow hijack. A heap overflow overruns a buffer in dynamically allocated memory, corrupting allocator metadata or neighbouring objects, which usually takes more steps to weaponise. Both are out-of-bounds writes with different neighbours.

Do stack canaries and ASLR fix the bug?

No. They make a given overflow harder to exploit reliably. A canary detects a return-address overwrite before the return and aborts. ASLR hides where code and data live. Neither prevents the out-of-bounds write itself, so the underlying defect remains until the unchecked copy is bounded or the code is moved to a memory-safe language.

Why are buffer overflows still common decades after being understood?

Because the code most prone to them, operating systems, browsers, and other systems software, is written in C and C++ for the low-level control those languages give, and that same control removes the automatic bounds checking that would catch the bug. The existing body of such code is enormous and cannot be rewritten quickly, so the class persists even though the cause is well known.

How do fuzzing and code review catch these bugs?

Code review looks for unbounded copies and concatenations into fixed buffers and for length calculations that can be wrong. Fuzzing feeds oversized and malformed input to code that parses external data, exercising the exact off-by-one and unchecked-length paths that reading the code by hand tends to miss. The two are complementary, and parsers of untrusted input deserve both.

What is the single most effective defence?

Bounding every write at the source, so a copy never exceeds the size of its destination, removes the defect rather than mitigating its consequences. For new code, choosing a memory-safe language removes the whole class where manual memory management is not required. The mitigation layers are valuable on top of these, and they are a tax on exploitation rather than a fix for the bug.

Buffer overflows persist because the code that contains them, systems software written in C and C++, is exactly the code the world runs on and cannot casually replace. The path forward is boring and effective: bound your writes, compile with every protection on, fuzz the input paths, and move new code to memory-safe languages so the next generation of parsers never has the bug to begin with. For a live view of which memory-corruption flaws are actually being exploited, see our Exploit Intelligence dashboard.

Sources & further reading

Sharetwitterlinkedin

Related guides