Background
Foundations before the bugs: why security matters, the threat-model reasoning tool, and the x86 machine model.
The software that runs the world is memory-unsafe
Follow any request through a real system, from the network, through the browser and server, down web protocols, into the client and server operating systems, and back to the user. An enormous fraction of that stack is written in C and C++, languages that give the programmer raw control over memory and, with it, the ability to make catastrophic mistakes. C and C++ are memory-unsafe: nothing stops code from reading or writing outside the region it was given.
A large share of those bugs are memory-safety issues. Microsoft and Chromium have each put the figure near 70% of their serious vulnerabilities {{ cite.msrc_bluehat }} {{ cite.chromium_memsafe }}. Others are privilege escalation: winning rights beyond those you were granted, such as an ordinary account reaching root or the kernel. And the products with the most vulnerabilities on record are overwhelmingly operating systems, precisely the C/C++ code everything else trusts.
{{ productOsShare }} are operating systems. The one application in the list is a browser, which is itself a large C++ codebase that parses hostile input all day.
Benign-but-buggy code
The threat model for this whole lecture is important, and a little surprising: the application programmer is well-intentioned. They are not malicious. They simply make mistakes. The gap we exploit is between the intended program behaviour and the program's actual behaviour on real hardware.
Everything in the gap is a memory vulnerability. They split into two families:
A primer on the x86 machine model
These bugs live in the gap between what C says a program does and what the machine actually does, so you need a rough picture of the machine. Three ideas carry almost all the weight. If you want the full account rather than the working minimum, two good assembly guides are in the references {{ cite.uva_x86 }} {{ cite.berkeley_x86 }}.
Registers are a handful of named slots inside the CPU itself. They hold single values, they are far faster than memory, and arithmetic happens in them rather than in RAM. Three of them matter here:
That anchor is what makes the second idea work. An instruction rarely names an absolute address; it names a register and an offset from it. Read mov 0x12[ebp], ecx as take the value in ECX and store it 0x12 bytes away from wherever EBP points. Every local variable and every argument is reached this way, which is why a function can be compiled once and still work no matter where on the stack it happens to run. You will also see this written mov [ebp+0x12], ecx; the two mean the same thing.
The third idea is the one the whole lecture rests on, and it is a collision of two directions. The stack grows downwards, towards lower addresses: each new call sits below the one that called it. But writing into an array runs upwards, from index 0 towards higher addresses, the way arrays work everywhere. So a buffer sitting in a stack frame fills in the opposite direction to the one the stack grew:
The stack grew downwards to make room for buf, but buf fills upwards. Keep writing past buf[49] and you do not run off into unused space: you run straight back over the saved EBP and then the return address, the two things the function needs in order to return correctly.
That is the whole trick. Everything after this is detail about how to get there and what to put in the return address once you can.
Stack frames: how a call is laid out
Consider f() calling g(x, y), where g reads into a local char buf[50]. Step through how the stack is built:
int f() {
...
g(x, y);
}
int g(int x, int y) {
char buf[50];
scanf("%s", buf); // no length limit!
}
Spatial Memory Vulnerabilities
The spatial memory errors themselves, code that reads or writes outside an object's bounds: buffer overflows, format-string bugs, and integer overflow.
Defining spatial memory safety
A program is memory-safe when every access to an object stays inside two boundaries: the object’s bounds, and the object’s lifetime. Break the first and you have a spatial violation. Break the second and you have a temporal one. Everything in this course is one of those two {{ cite.sok_memory }}.
buf[60] into a 50-byte buffer breaks it. (This lecture.)One thing that is not a memory-safety requirement, and it is worth being precise about: a memory leak. Forgetting to free memory wastes it, and eventually the program may run out, but every access the program makes is still in bounds and still within lifetime. A leak is a correctness and availability problem, not a safety violation. Attackers exploit the two failures above; they do not exploit tidiness.
Those two failures give the taxonomy the rest of the course hangs on:
Buffer overflows
int g(int x, int y) {
char buf[50];
scanf("%s", buf); // reads until whitespace, however long that takes
}
The bug is simple: scanf("%s", buf) keeps writing until it hits whitespace, with no regard for the 50-byte size of buf. Feed it more than 50 bytes and the write spills upward, over the saved frame pointer (the caller’s base pointer, saved on entry so it can be restored), and then over the return address. Watch it happen:
None of this is new. Aleph One laid the technique out step by step in Phrack in 1996 {{ cite.aleph_one }}, and the paper is still the clearest first read on the subject. That it still works, three decades on, is the point of this course.
The instant the return address, the slot holding where execution resumes after the call, is overwritten, the program's control flow is in the attacker's hands. When ret executes, it pops that slot into EIP and jumps there. Overwrite it with 0x41414141 (all A's) and you get a crash; overwrite it with a chosen address and you get an exploit. That hand-off of control to the attacker is the subject of Lecture 3, Control-Flow Hijacking.
What stops it
The bug is not that scanf exists, it is that "%s" names no limit. Give it one and the write cannot leave the buffer:
scanf("%49s", buf); // at most 49 chars plus the terminator
fgets(buf, sizeof(buf), stdin); // or take the size from the buffer itself
Note what the second form does: it derives the bound from sizeof(buf) rather than from a number you typed. A literal 49 is a fact about the code that has to be kept true by hand; if someone later widens buf to 100 bytes, the literal silently becomes wrong in the safe direction, and if they narrow it, wrong in the dangerous one.
Compilers and operating systems also push back. A stack canary puts a known value between the locals and the saved return address and checks it before returning, so a contiguous overflow is detected rather than obeyed {{ cite.stackguard }}. Step through what that does, and what it does not:
The third state is the one to remember. A canary raises the cost of the overflow in this lecture, and says nothing at all about an attacker who never crosses it. Lecture 3 takes that argument up properly. In advisories, this bug class is CWE-787 {{ cite.cwe787 }}.
Program 1 in the lab below is this bug in a compilable form, and it prints its own stack layout so you can see exactly how far the overflow has to travel. Levels 1 to 5 take you from reading that layout to steering the program into a function it never calls, and then to putting the defences back and watching which of your attacks survive.
Format-string bugs
Functions like printf and scanf are variable-argument functions. The format string (e.g. "%s%d") is interpreted at runtime by the callee to decide how many further arguments to read and how. If "%s%d" appears, printf expects two more arguments: a string pointer, then an integer.
Now look at this program. One line is a serious vulnerability. Before you read on, try to spot it: click the line you think is the bug.
If the user types %d %d %d ... as their “username”, printf assumes those arguments were passed and starts reading successive stack slots, walking straight into magicNumber. Step through it:
Reading is the small half
Everything so far leaks: %x and %d print stack slots the caller never passed. That is bad, but it is only a read. The specifier that makes format strings as dangerous as a buffer overflow is %n, which does not print anything at all. It writes: it stores the number of characters printed so far into an int * taken from the argument list.
int written;
printf("hello%n", &written); // written == 5, nothing printed for %n
Now put that together with the bug. The attacker controls the format string, so they choose both the pointer %n writes through, by placing an address in the buffer, and the value written, by padding the output with something like %200x so the running character count reaches whatever they want. Choose the address and choose the value, and this is the same write-anything-anywhere primitive a heap overflow reaches by a different road. That is why this belongs beside buffer overflows rather than in a footnote about information leaks.
What stops it
This one has a genuinely one-line fix. The mistake is passing user data as the format; pass it as an argument instead:
printf(localStr); // the user picks the specifiers printf("%s", localStr); // the user is now just data
Because the pattern is so recognisable, compilers find it for you: -Wformat-security warns on exactly this shape, and many builds now make it an error. The class is CWE-134 {{ cite.cwe134 }}.
On 32-bit x86 all arguments are passed on the stack, so %x specifiers walk the stack directly. On 64-bit x64 the first five arguments live in registers, so the first few specifiers read registers before reaching stack memory. The bug is the same; the plumbing differs.
Program 2 in the lab below is this bug, ready to compile. Levels 6 to 9 go from reading stack words back, to printing a string the program never offered you, to writing a value of your choosing into one of its variables. All of it is typed as plain text at a prompt, with no debugger required.
Do it yourself
Reading about an overflow is not the same as watching one happen to a program you compiled a minute ago. Below are two short programs and nine tasks that climb from reading a stack layout to writing a value of your choosing into a variable the program never meant to expose. Every task has been run against these exact sources on x86-64 gcc, so the numbers quoted are real rather than illustrative. The structure follows the SEED labs {{ cite.seed_labs }}, adapted to run in a browser tab.
Both programs print their own addresses and offsets. That is a deliberate simplification: finding addresses is a separate problem that needs a debugger and a disabled ASLR, and Lecture 3 handles it. Removing it here leaves the part worth practising now. Everything you need to type is ordinary text, so an online compiler with a stdin box is enough.
{{ p.code }}
{{ p.buildNote }}
{{ l.taskNode }}
Hint. {{ l.hintNode }}
You have it when. {{ l.winNode }}
Go further. {{ l.moreNode }}
What to carry forward
- Memory errors come in two families: spatial (out of bounds) and temporal (out of lifetime).
- In the worst case they give an attacker the power to read or write any value anywhere in memory.
- Hardware does not give you memory safety; neither does C/C++.
- How attackers turn these bugs into control (code injection, code reuse, ROP) is its own topic: Lecture 3, Control-Flow Hijacking.
{{ course.notes }} · Instructors: {{ course.instructors }}
References
Links last checked July 2026. CVE Details blocks automated requests, so {{ cite.nvd_products }} was verified by hand rather than by script.