GCC'nin Stack Koruması
C dilinde, fonksiyon argümanları, yerel değişkenler (statik olmayan) ve fonksiyon çağrısı bittiğinde programın çalışmaya devam edeceği dönüş adresi stack'de tutuluyor. Program hafızasının LIFO mantığıyla çalışan bu bölümü, fonksiyonların birbiriyle iletişimi ve program akışının sağlanması açısından önemli. Stack'in kötü niyetli kullanım veya programlama hatası nedeniyle bozulması, programın hatalı sonuç üretmesi, çökmesi veya yetkisiz işleme izin vermesi gibi çeşitli olumsuzluklara neden olabilir. Modern C derleyicileri, bu tip olumsuzlukları kontrol altında tutmak için bir yöntem kullanıyor.
Öncelikle sorunu görmek açısından, aşağıdaki programı gcc'nin bu özelliğini kapatarak (-fno-stack-protector seçeneği ile) derleyip, programın assembly koduna bakalım.
|
#include <stdio.h>
|
|
|
|
void return_input()
|
|
{
|
|
char buffer[12];
|
|
gets(buffer);
|
|
printf("%s\n", buffer);
|
|
}
|
|
|
|
int main(int argc, char const *argv[])
|
|
{
|
|
return_input();
|
|
return 0;
|
|
}
|
Yukarıdaki fonksiyonda, gets
fonksiyonu komut satırından bir metin alıp, buffer
içine kaydedecek. Eğer,
okunan metin 11 haneden daha fazla ise, buffer için ayrılan hafızanın dışına yazmaya devam edecek. fno-stack-protector ile
derlenmiş programdaki, return_input
fonksiyonu nasıl derlenmiş bir göz atalım.
Burada herşey normal. İlk iki satır C'deki standart fonksiyon çağırma geleneğine göre ebp
register'ını ayarlıyor
. Daha sonra stack üzerinde 0x28 (40) byte yer ayrılıyor. Devamı ise,
gets
ve puts
fonksiyon çağrıları ve geri dönüş. Programı gdb ile çalıştırıp, buffer ve frame pointer
adreslerine bakabiliriz. Bende, buffer
, 0xffffd704
adresinde, ebp
ise, 0xffffd718
adresinde çıktı. Bu durumda,
buffer
üzerine 0x14 (20) byte yazılması haline, return_input
fonksiyonu için ayrılmış alanın dışına çıkılacak. Eğer
fonksiyonda başka yerel değişkenler olsaydı, bu değişkenlerin üzerine yazılması ihtimali de ortaya çıkacaktı. Peki,
fonksiyon için ayrılmış alanın dışında ne var? Öncelikle, bu fonksiyonu çağıran fonksiyonun (bu örnekte main) frame pointer'ı ve
return_input
'un dönüş adresi (main fonksiyonunda, return_input
çağrısından sonraki adres), daha sonra, çağıran fonksiyonun yerel değişkenleri veya fonksiyon
çağrısından önce kaydettiği register değerleri olabilir. Bunların üzerine yazılması halinde, en iyi ihtimalle programın geçersiz
bir adrese zıplamaya çalışıp çökmesini umabiliriz. Kötü ihtimal ise, programa verilen kötü niyetle tasarlanmış bir metin yüzünden,
programın yetkisiz/zararlı işlemler yapması.
Aynı fonksiyonun stack protector aktif olarak derlenmiş hali aşağıdaki gibi;
Burada fonksiyon ilkine göre biraz daha uzun görünüyor. Anlaşılmasına yardımcı olması için, bunun bir benzeri C ile yazabiliriz.
Yukarıdaki program fno-stack-protector ile derlendiğinde (kendi stack protector mantığımızı yazdığımız için)
eğer komut satırından okunan metin 12 haneden daha az ise, return_input
fonksiyonundan dönülecek. Eğer alınan
metin array sınırları dışına çıkarsa, return_input
fonksiyonu asla dönmeyecek ve hata mesajı gösterilip program
sonlandırılacak. Bunu sağlamak için array'in hemen arkasına rastgele bir değer atıyoruz. Fonksiyondan dönmeden önce,
bu değerin aynı kaldığını test ediyoruz. Eğer bu değer değiştiyse, programın güvenilmez olduğuna karar verip,
çalışmayı durduruyoruz.
Yukarıdaki C programına bakarak, assembly çıktısını daha rahat inceleyebiliriz. Assembly çıktısındaki mov eax,gs:0x14
satırı, glibc'nin program başlangıcında, main
çağırılmadan önce ayarladığı rastgele sayıyı (canary) alıyor ve mov DWORD PTR [ebp-0xc],eax
satırı bunu array'in sınırına kaydediyor. gets
ve puts
çağrısından sonra ise, mov eax,DWORD PTR [ebp-0xc]
satırı, array
sınırındaki değeri okuyor ve xor eax,DWORD PTR gs:0x14
satırı bu değerin değişmediğini teyit ediyor. Eğer bir değişme varsa,
glibc tarafından sağlanan __stack_chk_fail
fonksiyonuna atlıyoruz. Bu fonksiyonun amacı, detaylı hata mesajı yazdırıp, programı
sonlandırmak.
Yukarıdaki örneği, bir de Visual Studio ile derleyip (Visual Studio 14, 32bit cl.exe -Z7 ile) gcc ile karşılaştırabilriz.
Visual Studio da gcc gibi, stack üzerinde buffer overflow kontrolü yapıyor.
Toparlamak gerekirse, GCC ve Visual Studio (ve muhtemel diğer modern C derleyicileri) stack üzerindeki buffer overflow'ların yan etkilerini kontrol altında tutmak için ekstra kod üretiyorlar. Burada akılda kalması gereken husus, bu yöntemlerin buffer overflow'u önlemek amaçlı değil, buffer overflow sonucunda programın yanlış noktaya zıplamasını önlemek. Böylece, en klasik exploit yöntemlerinden birinin uygulanması bir hayli zorlaşmış oluyor. Yine de, özellikle kritik programlarda, derleyiciden medet ummak yerine, programın overflow'a izin vermeyecek şekilde tasarlanması veya, C#, Java, Python gibi hafıza yönetimini kendi yapan bir dilde yazılması çok daha akıllıca bir seçim olacaktır diye düşünüyorum.