.dtors / .fini_array overwrite¶
Bir ELF destructor function-pointer array'ini overwrite ederek C runtime'ın program sonlandığında attacker-controlled kod çağırmasını sağla.
Mechanism¶
Note
Bir process normal olarak sonlandığında, C runtime termination section'larını
execute eder. Eski gcc'de bu .dtors'tu; modern ELF'te .fini_array'dir (artı
.fini). __libc_csu_fini, __fini_array_start ile __fini_array_end
arasındaki function pointer'ları iterate eder ve her birini ters sırada,
argümansız çağırır. Array, writable bir data section'ında yalnızca bir code
pointer listesidir — dolayısıyla bir entry'ye ulaşan herhangi bir
write-what-where, "program çıkar"ı "program benim pointer'ıma jump eder"e
çevirir.
Invariant: exit'teki control flow, return-address integrity'si ile değil bir data tablosuyla sürülür. İki klasik kullanım:
- Direct hijack — bir entry'yi shellcode / bir
one_gadgetile overwrite et. - Re-entry / loop hilesi — bir entry'yi
main'in adresiyle (ya da__libc_csu_fini'nin saklı return slot'uyla) overwrite et; böylece program exit'te etkin bir şekilde yeniden başlar ve one-shot bir bug'a (örn. bir format string) karşı ikinci bir geçiş sağlar — ASLR/NX bu şekilde iteratif olarak yenilir.
Warning
Full RELRO altında .fini_array section'ı (tıpkı GOT gibi) read-only
map'lenir ve bu vektörü öldürür. Yalnızca Partial/No RELRO ile çalışır. Eski
.dtors, 0xffffffff/0x00000000 sentinel marker'larını kullanırdı; klasik
saldırı null sentinel'i bir shellcode adresiyle overwrite ederdi.
Walkthrough¶
Array'i bul ve writable olup olmadığını kontrol et:
$ readelf -SW ./vuln | grep -E 'fini_array|dtors'
[19] .fini_array FINI_ARRAY 080496dc 0006dc 000008 00 WA 0 0 4
$ checksec ./vuln
RELRO: Partial RELRO # .fini_array is writable
Format-string re-entry payload'ı (CODEGATE-tarzı): bir %hn write'ını .fini_array
entry'sine yönelt ve onun düşük 16 bit'ini main'in alt yarısıyla değiştir:
from pwn import *
fini = 0x080496dc # .fini_array[0]
# write low 2 bytes of main() into fini via positional %hn
payload = b"%" + str(main_lo).encode() + b"c%11$hn"
payload = payload.ljust(0x20, b'A') + p32(fini)
p.sendline(payload)
Beklenen davranış: exit'te, legitimate destructor yerine main() yeniden invoke
edilir — binary "yeniden başlar" ve birinci geçişte libc leak'lemenize, ikinci
geçişte printf@GOT'u system ile overwrite etmenize olanak tanır.
Ters sıra neden önemli
__libc_csu_fini index'ini __fini_array_end'den __fini_array_start'a doğru
azaltır; son register edilen destructor ilk çalışır. İki entry ile loop'un
stack'teki saklı return address'ini __libc_csu_fini'ye geri işaret edecek
şekilde overwrite edebilir ve onu yeniden iterate ettirebilirsiniz — temiz, sonsuz
bir exploitation loop'u.
Mitigation¶
- Full RELRO (
-Wl,-z,relro,-z,now).fini_array'i read-only yapar. - PIE + ASLR, target adresini hesaplamanın maliyetini yükseltir.
- Full RELRO'nun varsayılan olduğu hardened modern toolchain'lerde teknik ölüdür; bunun yerine exit-handler abuse veya FILE-stream teknikleri'ne pivot et.