Machine-Level Programming/Memory Layout and Buffer Overflows

Lesson 2.71,660 words

Memory Layout and Buffer Overflows

The process address space — text, data, heap, and stack — and the classic vulnerability it enables. A stack buffer that is written past its end can overwrite the saved return address and redirect ret, so we sketch the mechanism defensively and then the three standard protections: stack canaries, a non-executable stack, and address-space layout randomization.

╌╌╌╌

Every fact from this module — the stack growing down, the return address call pushes, arrays as bare base-plus-offset with no bounds check — converges on one of the most consequential bugs in computing: the buffer overflow. This closing lesson lays out how a process arranges its memory, shows how writing past a stack array can overwrite the return address and redirect ret, and covers the standard defenses. The framing throughout is defensive: the goal is to understand the mechanism so you can recognize and prevent it.

The process address space

A running process sees its virtual memory partitioned into regions, each holding a different kind of data and growing in a fixed direction. From the lowest addresses upward: the text segment of machine code, the data segments of globals, the heap that grows up as malloc hands out memory, and at the top the stack that grows down.1

RegionAddress rangeGrowsContentsPermissions
Stackhighdownframes, locals, return addressesRW, NX
Heaplow → upupmalloc allocationsRW, NX
Datalowfixedglobals, staticsRW
Textlowestfixedmachine codeR, X
A process address space (high addresses at top). Read-only code and globals sit low; the heap grows upward and the stack grows downward, leaving a large unused gap between them.

The stack and heap grow toward each other into the same gap, which is why their opposing directions matter: each has room to expand without a fixed boundary between them. Local arrays live in the stack region, right alongside the saved return addresses from procedures — the adjacency that makes the overflow possible.

How an overflow overwrites the return address

A stack-allocated array sits in the current frame at addresses below the saved return address, and the array grows toward higher addresses as it fills, toward the return address. C array writes carry no bounds check: a copy that writes more bytes than the array holds keeps going into whatever lies above it — and what lies above it is the return address.

echo.cc
void echo(void) {
    char buf[8];       // 8 bytes on the stack, below the return address
    gets(buf);         // gets() writes input with NO length limit
    puts(buf);
}

The vulnerability comes down to layout. The buffer is allocated low in the frame, and the return address that call pushed sits just above it. Input fills the buffer upward toward higher addresses, straight at the return address. Nothing in hardware separates the two; they are adjacent bytes in the same frame.

The fatal adjacency. buf is allocated low in the frame; the return address pushed by call sits just above it. Input fills buf upward, toward the return address, with no boundary between them.

gets writes characters until a newline with no notion of buf's size, so an input longer than 8 bytes spills past buf's end. The figure shows the frame before and after such an overrun.

A stack frame before and after an overrun. Writing past buf's 8 bytes continues upward into the saved return address; ret then jumps wherever those overwritten bytes point.

The distance is countable, which is what makes the bug mechanical. Let the buffer have size bytes at %rsp, with bytes of other locals and saved registers above it; on x86-64 the return address is 8 bytes at

In this minimal frame , , so the return address sits at 8(%rsp). An input of exactly 8 bytes fills buf and stops at the return address's doorstep; input bytes through land on the 8-byte saved return address, one input byte per address byte. To redirect ret to 0x401156 an attacker supplies 8 filler bytes followed by 0x401156 little-endian (56 11 40 00 00 00 00 00). No guessing about layout is needed once is known; the overflow is a straight byte-for-byte overwrite.

Byte offsets in echo's frame. buf occupies input bytes 1-8; bytes 9-16 overwrite the 8-byte return address one-for-one. Supplying a target address as input bytes 9-16 redirects where ret jumps.

When echo finishes, its ret pops the now-corrupted bytes into %rip and the processor continues executing wherever those bytes point. The payload structure that achieves this is fixed once the offset is known:

Algorithm:
  1. 1
    payload := shellcode
    bytes to run, land in buf
  2. 2
    payload += filler up to length (b + g)
    pad to the return-address slot
  3. 3
    payload += little_endian(&buf)
    overwrite ret_addr, point back at buf

In the benign case an arbitrary overwrite is a crash; in the exploit case the overwritten return address points back into the buffer, where the same input placed bytes meant to run as instructions. That is stack smashing in one sentence: input that is both data overflowing the buffer and a redirected return address pointing at attacker-chosen code. We keep the sketch conceptual deliberately; the point is the mechanism and its prevention, not a working exploit.

The fix at the source

The root fault is the unbounded read, and the first defense is to never write one. gets cannot be used safely: it takes no buffer size, so it will always overflow a buffer given long enough input, which is why it was removed from the C standard entirely. Its bounded replacement is fgets, which takes the destination size and stops one byte short of it to leave room for the terminator.

echo-fixed.cc
void echo(void) {
    char buf[8];
    fgets(buf, sizeof(buf), stdin);   // reads at most 7 bytes, then '\0'
    puts(buf);
}

The same discipline applies across the unsafe/safe pairs: strcpystrncpy, sprintfsnprintf, strcatstrncat. Each safe form takes a length and refuses to write past it. The runtime defenses below exist because not all code can be rewritten, and even careful code has bugs, but a bounded read is the cleanest place to stop an overflow: it never happens.

Three runtime protections

The classic attack is a chain of three steps: overflow the buffer, reach and overwrite the return address, then have ret jump into attacker code on the stack. Each defense snaps one link, and breaking any one link breaks the chain.

The attack as a three-link chain and the defense that breaks each link. A canary guards the path to the return address, NX stops execution on the stack, and ASLR hides the address to aim the overwrite at.

Modern systems make this attack far harder with three layers of defense, each attacking a different link in the chain.2

Stack canaries. The compiler places a random canary value between the local buffers and the saved return address, and checks it just before ret. An overflow that reaches the return address must pass through the canary first, corrupting it; the mismatch is detected and the program aborts before the poisoned ret runs.

The stack-canary mechanism. A random value sits between the buffer and the return address; an overflow reaching the return address must first alter the canary, and the pre-ret check catches the mismatch.

The compiled code makes the check visible. With -fstack-protector, gcc wraps the body in a prologue that stashes the canary and an epilogue that verifies it. The canary itself comes from %fs:40, a per-thread value the operating system seeds with random bits at thread start.

canary.sassembly
echo:
        subq    $24, %rsp          # allocate the frame
        movq    %fs:40, %rax       # load the per-thread canary
        movq    %rax, 8(%rsp)      # place it just below the return address
        xorl    %eax, %eax         # scrub the copy from the register
        # ... buf lives at (%rsp); the vulnerable read runs here ...
        movq    8(%rsp), %rax      # reload the canary
        subq    %fs:40, %rax       # compare with the original
        jne     .Lfail             # any difference: it was overwritten
        addq    $24, %rsp
        ret
.Lfail:
        call    __stack_chk_fail   # abort; never returns

An overrun large enough to reach the return address must cross the canary slot at 8(%rsp) and change it, so the subq %fs:40, %rax leaves a non-zero result and jne diverts to __stack_chk_fail. The program dies with a diagnostic instead of returning through a corrupted address.

Non-executable stack (NX). The hardware can mark the stack region as non-executable, so even if ret lands there, attempting to run bytes from the stack faults. This breaks the execute code I placed in the buffer half of the classic attack outright.

Address-space layout randomization (ASLR). The loader places the stack, heap, and libraries at randomized addresses on each run, so an attacker cannot reliably predict the address to point the corrupted return at. The hard-coded target that a fixed layout would permit is no longer knowable in advance.

ASLR across two runs of the same program. The stack, heap, and library bases are shifted by different random offsets each time, so an address hard-coded from one run misses in the next.
ProtectionWhat it attacksEffect
Stack canaryreaching the return addressoverflow corrupts canary, caught pre-ret
Non-executable stackrunning code in the bufferexecution on the stack faults
ASLRpredicting the target addresslayout differs each run, no fixed target

None is a complete defense alone — canaries can be leaked, NX is countered by reuse of existing code, ASLR by information disclosure — but together they raise the cost of an attack enormously, which is why all three are on by default in current toolchains and operating systems.

The arms race after NX

The phrase NX is countered by reuse of existing code names an entire technique the textbook only alludes to: return-oriented programming (ROP). Once a non-executable stack forbids running attacker-supplied bytes, the attacker stops injecting code and instead chains together short snippets — gadgets — that already exist in the program's executable text, each ending in a ret. By overwriting the stack with a sequence of gadget addresses, the corrupted ret jumps to the first gadget, whose own ret pops the next address off the still-attacker-controlled stack, and so on, stitching existing instruction fragments into a new program without writing a single executable byte. Shacham's 2007 paper showed the available gadgets in a normal C library are Turing-complete, which is why NX alone does not end the game.3

The hardware answer, now shipping, is control-flow integrity enforced in silicon. Intel's CET (Control-flow Enforcement Technology) and ARM's equivalent add a shadow stack: a second, protected stack that holds a duplicate copy of every return address. call pushes to both stacks; ret checks that the return address on the ordinary stack still matches the shadow copy, and faults if an overflow has changed one but not the other. Because the shadow stack lives in memory the ordinary overflow cannot reach, it detects the return-address corruption that canaries only probabilistically catch. CET's companion feature, indirect-branch tracking, similarly constrains indirect jumps to legitimate targets, cutting off the jump-oriented cousin of ROP. Together they represent the current frontier of the same battle this lesson opened with: the return address is the crown jewel, and each defensive generation guards it more tightly than the last.4

Footnotes

  1. Bryant & O'Hallaron, CS:APP, §3.10.1 — the run-time memory image: read-only code, initialized and uninitialized data, the upward-growing heap, and the downward-growing stack.
  2. Bryant & O'Hallaron, CS:APP, §3.10.4 — Thwarting Buffer Overflow Attacks: stack canaries (stack protector), non-executable memory regions, and address-space layout randomization.
  3. Shacham, The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86), ACM CCS (2007) — return-oriented programming, chaining existing ret-terminated gadgets into arbitrary computation despite a non-executable stack.
  4. Intel, Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 1 (2023), §17–§18 — Control-flow Enforcement Technology: the shadow stack that mirrors return addresses and faults on mismatch, plus indirect-branch tracking.

╌╌ END ╌╌