Step 62. Experiment: Buffer Overflow — Where the Overflowing Input Goes

Step 62. Experiment: Buffer Overflow — Where the Overflowing Input Goes

Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★★☆ | Estimated time: 4 hours

Prerequisites: Steps 56–61 complete. You know that arrays are adjacent and that C doesn’t check boundaries. You can use bash and gcc on WSL Ubuntu.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.

  • What you need: a WSL Ubuntu terminal, gcc. The verification environment is Ubuntu 24.04, gcc 13.3.0, x86-64.
  • Caution: every target of today’s experiments is a program you just wrote yourself. Even with the same principle, applying it to someone else’s program or service is a crime.

Today we reproduce the most famous vulnerability in hacking history ourselves: the buffer overflow. All the C lessons so far were, in truth, for this day. Arrays being adjacent (Step 59), C not checking boundaries (Steps 59, 61), local variables living side by side on the stack (Step 60) — combine these three and one incident follows automatically. "Take input longer than the box, and the neighboring variable gets overwritten." Today’s goal is not attack but observation. We pour a long input into an eight-slot box and watch what happens to the variable next door.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain what a buffer is and why it overflows
  • Observe and record the scene where overflowing data overwrites a neighboring variable
  • Calculate "from which-th character it reaches" using the address distance between two variables
  • Confirm the compiler’s stack protection detecting an overflow
  • Block the same input with bounded input (%7s, fgets)

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C language, WSL Ubuntu bash, gcc 13.x (verified: 13.3.0, x86-64)
Today’s commands/options gcc -Wall -O0 -fno-stack-protector (compile for observation), python3 -c "print('A'*16)" | ./program (making long input)
Concepts needed Buffers, the absence of bounds checking, stack variable layout, \0 (string end marker), the stack protector

2-1. Buffers — Temporary Storage Boxes

A buffer is a memory box that holds data briefly. char buf[8] is "a buffer holding eight characters." When taking keyboard input, reading a file, or receiving network data, you put it in a box like this first.

The problem is that these boxes stand shoulder to shoulder with other variables on the stack.

2-2. The Absence of Bounds Checking — C’s Contract

C doesn’t scold you for reading outside the slots or writing outside them. It’s a contract of "trusting the programmer." The compiler passes even code that puts thirty characters into an eight-slot box.

Checking is the programmer’s job. And historically, programmers kept forgetting that check.

2-3. Overwriting — Where the Overflow Goes

Write sixteen characters into an eight-slot box — where do the remaining eight go? They don’t disappear. They overwrite the memory right next to the box, in order. The slots are adjacent, and the writing knows no end.

And if that neighboring slot happens to host a "password match flag" or an "admin privilege flag"? This is why a buffer overflow is not a mere bug but a vulnerability.

2-4. \0 — The End Marker of Strings

A C string carries a null character \0 (value 0) at its end to mark where the string ends. After receiving input, scanf’s %s writes one more \0 at the end.

So entering eight characters actually writes nine bytes. This one byte produces an unexpected observation in today’s experiment.

2-5. Designing the Observation — Building the Lab

Today’s experimental apparatus is this: place a variable called secret next to an eight-slot buffer, push a long input into the buffer, and watch whether secret changes.

Inside my safe code, in the lab of my own computer, we only observe. That is all of today.


3. Follow Along

3-1. The Apparatus — Two Neighboring Variables

Input (bof1.c)

#include <stdio.h>
#include <string.h>

int main(void) {
    char secret[8] = "SAFE";
    char buf[8];

    printf("secret before: %s (address %p)\n", secret, (void *)secret);
    printf("buf address: %p\n", (void *)buf);
    printf("Distance between the two: %ld bytes\n", (long)(secret - buf));

    printf("String for buf: ");
    scanf("%s", buf);

    printf("secret after: %s\n", secret);
    return 0;
}

Compile and run

gcc -Wall -O0 -fno-stack-protector bof1.c -o bof1
python3 -c "print('A'*8)" | ./bof1
secret before: SAFE (address 0x7ffe17063a08)
buf address: 0x7ffe17063a00
Distance between the two: 8 bytes
String for buf: secret after: 

(Verified 2026-09-09. Address values differ on every run. The distance can vary with the compiler and options.)

New commands and options:

  • -O0 — optimization off. Makes variables laid out in the structure you learned.
  • -fno-stack-protector — turns off modern gcc’s overflow detection device. Today our purpose is observation, so we turn it off on purpose. You’ll meet what this device is firsthand in 3-4.
  • python3 -c "print('A'*8)" | ./bof1 — instead of typing long input by hand, we make it with Python and pipe it in. This habit of crafting input with code will keep being used.

How to read the output: distance 8 bytes — eight steps from the buffer and you’re at secret. But we only put in eight characters (A×8), and secret became an empty string. The reason is 2-4’s \0. Eight input characters + the \0 scanf appends = nine bytes were written, and the ninth slot was exactly where secret’s first character lived. Since a string ends at its first \0, secret — its "S" overwritten by \0 — reads as a totally empty string.

Predict: if you put in sixteen characters (A×8 + B×8), what happens to secret? Calculate with the distance number, then confirm in the next experiment.

Why: the reason we look at addresses first is to predict "how many characters until it reaches." Predict → confirm. This order is the basic posture of analysis.

3-2. Overflow — The Moment of Observation

Run the same program again, this time with a long input.

python3 -c "print('A'*8 + 'B'*8)" | ./bof1
secret before: SAFE (address 0x7fff6e675938)
buf address: 0x7fff6e675930
Distance between the two: 8 bytes
String for buf: secret after: BBBBBBBB

(Verified 2026-09-09.)

How to read the output: sixteen characters went in. The first eight (A×8) filled buf; the last eight (B×8) crossed the boundary and overwrote secret entirely. "SAFE" vanished and B moved in.

We didn’t write a single line of code touching secret. What we wrote to the buffer crossed the boundary and changed the neighbor. This is the buffer overflow in the flesh.

Predict: if you put in A×8 + "HACK" (twelve characters), what does secret become? In testing, it became HACK — only secret’s first four bytes overwritten, with a \0 landing in the fifth slot. Check it yourself.

Why: this one screen is a scale model of countless incidents in hacking history. The moment input goes beyond data and changes neighboring data — you have now seen it with your own eyes.

3-3. Measuring the Distance to the Boundary — Studying by Calculation

Input (bof2.c)

#include <stdio.h>

int main(void) {
    int pin = 1234;
    char buf[8];

    printf("pin = %d (address %p)\n", pin, (void *)&pin);
    printf("buf address: %p\n", (void *)buf);
    printf("Distance: %ld bytes\n", (long)((char *)&pin - buf));
    return 0;
}

Compile and run

gcc -Wall -O0 -fno-stack-protector bof2.c -o bof2
./bof2
pin = 1234 (address 0x7ffdb8290f6c)
buf address: 0x7ffdb8290f64
Distance: 8 bytes

(Verified 2026-09-09. Addresses and distances vary by environment.)

How to read the output: a program that measures the distance between the target variable and the buffer in advance. If the distance is 8, overwriting of pin starts at the ninth byte. Calculating "how many bytes must I put in to reach the target" — that is the first step of analysis. Some environments show a negative distance (when the target is placed at a lower address than the buffer). In that case, swap the order and observe again. Either way, the principle is the same.

3-4. The Modern Compiler’s Protection — stack smashing detected

Take the same source as 3-1 and this time compile it with default options only, without turning off the protection.

gcc -Wall bof1.c -o bof1_prot
python3 -c "print('A'*8 + 'B'*8)" | ./bof1_prot
*** stack smashing detected ***: terminated
Aborted (core dumped)

(Verified 2026-09-09. In a Korean-locale environment the last line may appear translated, e.g. "중단됨 (core dumped)".)

How to read the output: Ubuntu’s gcc turns on the stack protector by default. It hides a watchdog value (a canary) behind the buffer, and if that value has been overwritten when the function ends, it aborts the program itself, declaring "the stack has been smashed." The overwrite that succeeded in 3-2 was detected here.

Why: the very existence of this device is a lesson. Most real programs are shipped wearing this protective membrane. We turned it off with -fno-stack-protector only for the observation experiment — remember both the device’s name and how to turn it off.

3-5. Fixing It into a Safe Version

Input (safe.c)

#include <stdio.h>

int main(void) {
    char secret[8] = "SAFE";
    char buf[8];

    printf("String for buf (max 7 chars): ");
    scanf("%7s", buf);

    printf("secret: %s\n", secret);
    printf("buf: %s\n", buf);
    return 0;
}

Compile and run

gcc -Wall -O0 safe.c -o safe
python3 -c "print('A'*8 + 'B'*8)" | ./safe
String for buf (max 7 chars): secret: SAFE
buf: AAAAAAA

(Verified 2026-09-09.)

How to read the output: even with the same attack input, %7s cut it at seven characters. Seven characters + \0 = eight bytes — a perfect fit for the buffer. secret is unharmed. For eight slots, %7s — the bound is "slot count minus 1." The more standard safe convention is fgets(buf, sizeof(buf), stdin).

Defense was not some grand technique — it was "code that knows the box’s size."

Think about it: %7s cut the input, but sixteen characters came in from the keyboard. The cut-off remainder B×8 stays in the input buffer, and if there’s another scanf later, it gets read there. Remember that "the debris of truncated input," too, is a factor that makes the flow hard to predict.

3-6. Reading an Overwritten Integer — The Moment Characters Become a Number

What value does an int variable overwritten by a buffer overflow become? Let’s see directly. (We declared the integer variable before the buffer so that, in the verification environment, the integer ends up above.)

Input (bofint.c)

#include <stdio.h>

int main(void) {
    int score = 100;
    char buf[8];

    printf("score address: %p, buf address: %p\n", (void *)&score, (void *)buf);
    printf("Distance: %ld\n", (long)((char *)&score - buf));
    printf("Input: ");
    scanf("%s", buf);
    printf("score = %d\n", score);
    return 0;
}

Compile and run

gcc -Wall -O0 -fno-stack-protector bofint.c -o bofint
python3 -c "print('A'*12)" | ./bofint
score address: 0x7ffce92f2a2c, buf address: 0x7ffce92f2a24
Distance: 8
Input: score = 1094795585

(Verified 2026-09-09.)

How to read the output: we put in twelve characters, and characters nine through twelve (A×4) overwrote score’s four bytes. But score became not 65 (‘A’s code) but 1094795585. Because an integer reads four bytes combined as a single number. Four ‘A’s (65,65,65,65) sitting side by side read as an int give that number — in hexadecimal, 0x41414141. Step 50’s hex comes back into use here.

For reference, we also confirmed by measurement that putting in just eight characters in the same experiment makes scanf’s single \0 overwrite score’s first byte, turning score = 0. The low byte of the integer 100 (hex 0x64) was erased to 0.

Think about it: if you could put in the bytes you want instead of ‘AAAA’, could you make score any value you want? Just engrave the fact that this calculation is possible — "overwriting can become not breaking but changing" — and today is enough.

Why: this discovery is the bridge from observation to analysis. Because you’ve seen that even the overwritten value can be computed.


4. Missions & Exercises

Mission — A Buffer Overflow Observation Report

Based on bof1.c, write an observation report.

  1. Input three lengths (8 chars, 12 chars, 16 chars) and record the change in secret for each
  2. Print the address distance between the two variables, hit "from which-th byte it gets overwritten" by calculation, then verify by experiment
  3. Feed the same three inputs to the program compiled with default options (protection on), and record from which length stack smashing detected appears
  4. Fix it into the safe version (%7s) and confirm the same input is blocked
  5. Answer at the report’s end: "Why is a buffer overflow dangerous — because input can go beyond data and do what?" A paragraph or more

Exercises

Problem 1. You entered eight characters into char buf[8], yet the neighboring variable became an empty string (3-1 verification). Shouldn’t eight characters be an exact fit? What got written in the ninth slot?

Problem 2. In 3-6, why did the int overwritten by four ‘A’s become 1094795585 instead of 65? Express it in hexadecimal too.

Problem 3. Compiling without -fno-stack-protector produced *<strong> stack smashing detected </strong>*: terminated. What did this device look at to notice the overflow, and why does it effectively block attacks?

Problem 4. When using scanf on an eight-slot buffer, the safe format is %7s. Why %7s and not %8s?


5. Model Answers & Completion Criteria

Mission Model Answer

An example observation report (2026-09-09, Ubuntu 24.04, gcc 13.3.0, with -O0 -fno-stack-protector):

[Distance measured] secret - buf = 8 bytes → secret gets overwritten from byte 9

[8 chars AAAAAAAA]  secret: "" (empty string) — the 9th byte \0 overwrote secret[0]
[12 chars A×8+HACK] secret: "HACK" — secret's first 4 bytes overwritten
[16 chars A×8+B×8]  secret: "BBBBBBBB" — all 8 bytes of secret overwritten

[Protection on, default compile] with 16-char input:
*** stack smashing detected ***: terminated

[Safe version %7s] same 16-char input → buf: AAAAAAA, secret: SAFE (unharmed)

How to verify: ① the calculation (distance + 1 = the byte number where overwriting starts) and the experimental results must match. ② The "why is it dangerous" paragraph must contain at least this much: "input can cross its own box and overwrite neighboring variables, and the overwritten value can carry meaning (flags, scores, addresses)."

Exercise Answers

Problem 1 answer. scanf’s %s writes one more string-termination marker \0 at the end of the input. So eight input characters actually write nine bytes, and the ninth byte (value 0) overwrote the first byte of the variable right next to the buffer. Since a string ends at its first \0, that variable reads as an empty string. Even an "exact-fit input" actually overflows by one byte.

Problem 2 answer. Because an int reads four bytes combined as a single number. ‘A’ is character code 65 (hex 0x41), and when that sits side by side in four slots, the whole four bytes become 0x41414141. Read in decimal, that is 1094795585.

Problem 3 answer. The stack protector (canary). If the watchdog value the compiler hid behind the buffer in advance has changed by the time the function ends, it judges an overflow and aborts the program. Since it cuts execution before the overwrite reaches more important things (like the return address), instead of the attack succeeding, the program "dies safely."

Problem 4 answer. Because one more \0 is appended at the end of the string. %8s writes eight characters + \0 = nine bytes, overflowing an eight-slot buffer by one byte. "Slot count minus 1" is the bound.

Completion Criteria Checklist

  • [ ] I can explain what a buffer and the absence of bounds checking are
  • [ ] I observed and recorded an overflowing input overwriting a neighboring variable myself
  • [ ] I can explain that the single \0 byte makes even an "exact-fit input" overflow
  • [ ] I calculated the overwrite point from the address distance and verified it experimentally
  • [ ] I know the stack protector’s role and what -fno-stack-protector means
  • [ ] I can defend with bounded input (%7s, fgets)
  • [ ] Mission: I completed the observation report (three lengths recorded + protection comparison + one-paragraph answer)

6. Common Pitfalls & Fixes

Wall 1. secret Doesn’t Change

Symptom: even with a long input, secret stays the same.
Cause: the compiler changed the variable layout order, or there’s padding (for alignment) in between, making the distance longer than expected.
Fix: print the two variables’ addresses and distance first (3-3). If the distance is 16, grow the input to 24 characters — put in distance + target size and it works. Observation equipment (address printing) is half the experiment.

Wall 2. It Dies with stack smashing detected

Symptom: the program aborts with a message like this (verified 2026-09-09):

*** stack smashing detected ***: terminated

Cause: you compiled with the protection still on. It’s Ubuntu gcc’s default.
Fix: for observation experiments, compile with gcc -Wall -O0 -fno-stack-protector. But that is for the lab only. Turning this device off in a program you actually ship is stripping away its protective membrane.

Wall 3. It Dies with a Segfault

Symptom: you made the input too long and got Segmentation fault.
Cause: the overflow went past the neighboring variable and overwrote something more important (like the address the function returns to).
Fix: that’s actually a good observation. It’s evidence that "if the overflow continues, the program’s flow itself breaks." Grow the input length little by little and find the boundary between "up to here it’s variables, from here on it’s collapse."

Wall 4. Fumbling Long scanf Input by Hand

Symptom: pasting a long string, the length goes off.
Cause: too long to type by hand.
Fix: make the input with Python. python3 -c "print('A'*16)" | ./bof1. Managing the input length precisely with code is what makes the experiment reproducible.

Wall 5. My Results Differ from the Book (Different Distance)

Symptom: the book says distance 8, but my environment shows 12, or a negative number.
Cause: variable layout and padding differ by compiler, version, and options. That’s normal.
Fix: that’s exactly why 3-3 measures the distance first. Write in your notes "in my environment: compile options, the two variables’ distance, the input length where overwriting began." This record becomes the baseline for rerunning the experiment.


7. Summary

Today’s Concepts

Concept One-line explanation
Buffer A memory box that holds data briefly — lives side by side with neighboring variables on the stack
Buffer overflow The incident where a write longer than the box overwrites neighboring memory
Absence of bounds checking C’s contract of not scolding you for writing outside the slots
\0 (null character) The string end marker — input length + 1 byte is actually written
Stack protector (canary) A compiler defense that detects overflow via a watchdog value behind the buffer and aborts
The fatal one byte Even an "exact-fit input"’s \0 overwrites the neighbor (3-1 verification)

Today’s Syntax and Commands

Code/command What it does
scanf("%s", buf) Dangerous input — no bound
scanf("%7s", buf) Bounded input (for 8 slots, 7)
fgets(buf, sizeof(buf), stdin) The more standard safe input
gcc -Wall -O0 -fno-stack-protector Compile for observation (lab only)
python3 -c "print('A'*16)" | ./program Craft long input with code and pipe it in

A Sense More Important Than Commands

What you saw today is the common principle behind countless intrusions stretching from the Morris worm that paralyzed the internet in 1988 through the decades since. "Input longer than the box + no bounds check = overwriting the neighbor." A vulnerability that survived so long precisely because it’s so simple. What we overwrote today was a toy variable, but what gets overwritten in reality is privilege flags, scores, and the program’s flow itself.

What matters is that the scene is not magic — you know it’s the teamwork of three physics: eight slots, a write with no boundary, adjacent variables. An attack you understand is not frightening. Because the way to block it is visible with the same eyes. Today’s observation report is the first record in your security studies of "touching a vulnerability with your hands." Keep it safe.


Once every box is checked, Step 62 is complete. Click the checkbox in the sidebar to save your progress.