Photo by Go Card USA, CC BY-SA 2.0, via Wikimedia Commons
The Disk
That floppy holds the source of the Morris worm. On November 2, 1988 the program on it spread across the early internet and jammed roughly 6,000 of the 60,000 machines connected at the time. Its author got the first conviction under the Computer Fraud and Abuse Act.
One of the ways it got in was a buffer overflow, and the version of that bug I actually learned came out of CS 354 at Wisconsin, in a program short enough to read in one sitting. Nothing here is a live security issue, since every compiler in the last twenty years stops it by default. It is just a good way to see what a stack frame is.
The Program
vuln.c
#include <stdio.h>
int main(void) {
int authorized = 0; // the guard: 0 means "no"
char name[8]; // room for 8 characters
printf("What is your name?\n");
scanf("%s", name); // reads a word, with no length limit
if (authorized)
printf("Access granted.\n");
else
printf("Hello, %s. Access denied.\n", name);
return 0;
}
Nothing sets authorized to anything but 0, so reading the source tells you no input can reach “Access granted”. Typing nine characters gets you there anyway.
Where the Variables Live
A function’s locals go in one block of memory called the stack frame. Compile to 32-bit assembly and the offsets are printed for you:
x86 assembly (Intel syntax)
mov dword ptr [ebp-8], 0 ; authorized = 0
lea eax, [ebp-16] ; &name[0]
call scanf
cmp dword ptr [ebp-8], 0 ; if (authorized)
name starts at ebp-16 and authorized sits at ebp-8, eight bytes further up. Number the bytes from name[0] and the frame reads name at 0x00 through 0x07, authorized at 0x08 through 0x0B, and the return address main jumps to at 0x0C.
scanf writes name starting at name[0] and works upward, toward authorized and then toward the return address. %s carries no length, so nothing stops it at eight.
Nine Characters
$ cc -fno-stack-protector -O0 vuln.c -o vuln $ ./vuln What is your name? adi Hello, adi. Access denied. $ ./vuln What is your name? AAAAAAAA Hello, AAAAAAAA. Access denied. $ ./vuln What is your name? AAAAAAAAA Access granted. authorized == 0x00000041
Eight A’s are already an overflow. scanf writes the NUL terminator after the word, so AAAAAAAA puts eight 0x41 bytes in name and a ninth byte, 0x00, into authorized[0]. Writing zero over a zero changes nothing, which is why eight is still denied.
The ninth A is the first byte that lands on authorized with something in it. authorized becomes 0x00000041, decimal 65, and if (authorized) is true for anything that is not zero.
The buffer’s size and the type of authorized are both live. Shrink name and the guard moves closer; make authorized a char and one byte is the whole guard. flip puts 0x00 at the bottom, if you would rather read the frame the way it is usually drawn.
Past the Guard
Keep typing and you run out of authorized. Above it, at 0x0C, is the return address, the value main pops into the instruction pointer when it returns. Flipping the guard gets you past an if. Overwriting the return address picks what runs next, and if your input is also sitting in memory somewhere you can address, it picks your bytes.
That is the version the worm used. BSD’s fingerd read a request line with gets, which has no length argument at all, into a 512-byte buffer on the stack.
Try It Yourself
Godbolt gives you the assembly. Paste vuln.c, pick x86-64 gcc, and set the options to -m32 -O0 -fno-stack-protector -no-pie. The mov [ebp-8], 0 and the lea for name are where the number nine comes from, read off instead of guessed.
To run it, plain 64-bit is fine:
cc -fno-stack-protector -O0 vuln.c -o vuln
echo AAAAAAAAA | ./vuln # Access granted.
Now drop the flag and let the compiler do its job:
cc -O0 vuln.c -o safe
echo AAAAAAAAAAAAAAAAAAAAAAAA | ./safe
# *** stack smashing detected ***: terminated
-fstack-protector is on by default now. It puts a known value, the canary, between the locals and the return address and checks it before returning, and it reorders the locals so arrays sit closest to the canary. That second part alone moves authorized below name, out of reach. ASLR shuffles the addresses on every run and the stack is mapped non-executable. None of it existed in 1988.