Lukas' Notes

security memory c

It is easy to read “buffer overflow” as a corruption bug: the program writes too many bytes into a fixed-size array, the extra bytes spill into whatever memory comes next, the program gets the wrong value for some neighbouring variable and misbehaves. That picture is correct for a buffer on the heap. For a buffer on the stack it misses the one fact that turns a data bug into a control-flow hijack.

The stack grows down, and the return address sits just above the locals

A locally declared buffer such as char buffer[10] = {0}; lives inside the function’s stack frame, which on x86-64 grows towards lower addresses. Inside that frame the layout is exactly defined: the local variables are placed just below the saved base pointer (RBP), which is just below the saved return address — the address of the instruction the CPU will jump to when this function returns. Writing up means walking into control data.

There is no padding between the buffer and the saved base pointer, and none between the saved base pointer and the saved return address. Memory is contiguous, and C pointers give the program byte-indexed access to it with no automatic bound enforcement. So a write that exceeds the buffer’s length walks straight up into the saved return address.

The escalation: a length bug hijacks control flow

The remaining ingredient is that an unsafe call lets the length of the write come from attacker input. gets(buf) reads until a newline; read(fd, buf, n) trusts whatever n the caller passed; strcpy(dst, src) trusts the null terminator inside src. In every case the program never measured the destination against the source, and the copy runs for as many bytes as the attacker’s input asked for.

When the saved return address is overwritten with one more byte of attacker data, here is what happens on ret:

  • if the new bytes form an invalid address — the function tries to return to a non-executable or unmapped region, and the CPU faults with a segmentation fault. The read(0, buffer, 100) demo crashes exactly this way; it is the noisy, benign failure mode;
  • if the new bytes form a valid, attacker-chosen address — the function returns into the attacker’s code, and a length bug has just become full control of the program counter.

The contrast is the whole lecture: a one-byte-too-many bug in an unsafe call is already bad; the stack layout is what makes it catastrophic.

Why this is 50% of all vulnerabilities

This mechanism explains the slide’s opening statistic — that roughly 50% of vulnerabilities patched each year are memory-safety issues, and roughly 70% of Chromium’s serious bugs. The C and C++ code that systems software is written in gives one bug class a direct path to control flow: a single missing bounds check, in a single unsafe call, against a single stack buffer, escalates from data corruption to arbitrary code execution, because the saved return address lives one slot away from the array the programmer forgot to bound. Removing the exploitation path (later lectures: canaries, ASLR, NX bit, CFI) does not remove the bug; it removes the escalation — which is exactly why the line of defence that matters is replacing unsafe languages with memory-safe ones. The mechanism is the saved return address sitting adjacent to the buffer, and the bug is anything that overflows one into the other.

The corrected model

A buffer on the stack is not just “nearby memory”. The stack frame an attacker overflows into is control data the CPU will jump through on return, defined by the calling convention’s contract that the saved return address lives exactly there and nowhere else. Hold that picture — “the saved return address sits one slot above my local array” — and a stack buffer overflow reads as control-flow hijack from the first byte past the end, not as data corruption plus an unrelated crash. The lecture cluster that builds on this insight is just the increments by which each defence narrows that walk, and the language-safety move that removes the bug at the root.