Arabellek taşması zafiyeti ilk defa 1988 yılında Robert Morris tarafından yazılan bir solucanda kullanılmıştır. Bu, aynı zamanda bilinen ilk solucandır. 1996 yılında Elias Levy'nin(Aleph1) Phrack Magazine'de yayınlanan makalesinde(Smashing the stack for fun and profit) arabellek taşması zafiyetleri ve istismar yolu adım adım anlatılmış ve bu olayla birlikte dünyada arabellek taşması saldırıları hızla yayılmıştır. Günümüzde bu saldırıları yapmak her ne kadar işletim sistemlerinin uyguladığı güvenlik önlemleriyle zorlaşmış olsa da hala pek çok exploit bu yöntemle yazılmaktadır. Bu makalede klasik arabellek taşması zafiyeti ve programcıların bu zafiyeti barındırmayan kod yazması için yapması gerekenler anlatılacaktır.
Bellek yönetimi
Arabellek taşması kısaca, değişkenlere varsayılandan büyük veriler girerek taşmaya sebep olunması ve bu sayede istenen bir kodun çalıştırılması olarak tanımlanabilir. Bu zafiyet Assembly, C ve C++ kullanarak yazılmış programlarda bulunmaktadır. Üst seviye programlama dillerinde runtime sırasında gerekli kontroller yapılmaktadır. Arabellek taşmasını anlayabilmek için öncelikle işletim sistemlerinin bellek yönetim sistemini anlamak gerekir.
Windows'ta tüm prosesler kendisine ait bir sanal adres uzayında bulunur. Runtime sırasında sanal bellek uzayındaki verilerle gerçek bellektekiler birbiriyle eşleştirilir. Bu sayede hiç bir proses bir başkasının alanına erişemez. 32-bit sistemlerde her bir proses için ayrılan sanal bellek alanı 4 GB'tır. Aynı alan 64-bit sistemlerde yaklaşık olarak 8 GB'tır. 32-bit sistemlerde bu alanın 0x00000000 ile 0x80000000 arasındaki bölümü proseslerin kendi bellek işlemlerini yaptıkları user space olarak kullanılırken, 0x80000000 ile 0xffffffff arasındaki bölümünü ise işletim sistemi kullanır ve bu alan kernel space olarak adlandırılır. Kernel space ancak işletim sistemi tarafından belirlenen fonksiyonlar aracılığıyla ulaşılabilir bir alandır.
Prosesler belleğe küçük bölümlere ayrılarak yüklenir ama genel olarak beş ana bölümden bahsedilebilir. Bunlardan text segmenti, program kodlarının assembly olarak bulunduğu yerdir. Text segmenti yazılabilir değildir. Bu segmente yazılmaya çalışıldığında program kapanır. Bu sayede kodun değiştirilmesi engellenmiş olur. Ayrıca text segmenti yazılabilir olmadığı için ihtiyaç duyduğu alan değişmez, dolayısıyla büyüklüğü sabittir. Data segmentinde değer atanmış değişkenler, bss segmentinde ise değer atanmamış değişkenler yer almaktadır. Örneğin; int x şeklinde oluşturulmuş değişkenler bss'te yer alırken, int x=0 ya da printf("yazi") gibi komutlarda yer alan değerler data segmentindedir. Bu segmentler yazılabilir oldukları halde boyutları değiştirilemez. Heap, bellekte runtime sırasında malloc, free gibi fonksiyonlarla işlem yapılmak istendiğinde kullanılır. Bu alanın boyutu değişkendir. Runtime esnasında kapladığı alan büyüyebilir veya küçülebilir. Heap segmenti yüksek belleğe, yani yığına doğru büyür.
Şekil 1 Proseslerin bellek kullanımı
Yığın (Stack)
İşletim sisteminde yeni bir thread oluşturulduğunda bellekte; fonksiyon parametrelerini, lokal değişkenleri ve fonksiyonların çalışması sonlandıktan sonra devam edeceği yeri saklamak için, yığın denilen alanlar oluşturulur. Her thread için iki yığın oluşturulur. Yığın aynı zamanda son giren ilk çıkar(LIFO - Last In First Out) prensibiyle çalışan soyut bir veri yapısıdır ve konumuz olan yığınlar da ismini paylaştığı bu veri yapısının prensibiyle çalışır.
Yığın da heap gibi değişken boyuta sahiptir. Küçük bellek adreslerine doğru büyüyen yığının en üstüne(düşük adrese) push komutuyla veri eklenirken, pop komutuyla son eklenen eleman çıkarılır.
Yığında en son eklenmiş verinin adresi SP(Stack Pointer) register'ında tutulur. Yani yeni bir veri eklendiğinde(push) veya eklenen son eleman çıkarıldığında(pop) SP'de tutulan adres değişir.
Arabellek taşması
Bir programda fonksiyon çağrıldığında, ilk önce fonksiyon parametreleri yığına yazılır. Ardından fonksiyondan çıkıldıktan sonra hangi komut çalıştırılacaksa, onun adresi yazılır. Sonrasında fonksiyona girmeden önceki değerini saklamak için BP'nin(Base Pointer) o andaki değeri ve ardından da BP'ye SP'nin değeri yazılır(function epilogue). Son olarak fonksiyondaki lokal değişkenleri tutmak için bir arabellek(buffer) alanı oluşturulur. Klasik arabellek taşması bu alanın taşırılarak IP'ye istenen bir değerin yazılması ve böylece program akışının değiştirilmesi durumudur.
Aşağıdaki fonksiyonda girilen parametreyle ilgili hiç bir denetleme bulunmamaktadır. Fonksiyon programcının beklediği biçimde, yani parametre olarak en fazla sekiz karakterlik bir dizi verilerek çağırılırsa yığın Şekil 2'deki gibi olacaktır.
Şekil 2 Fonksiyon çağırıldıktan sonra yığının görünümü
Ancak bu fonksiyon; arabelleği tamamen doldurup, ardından eski BP'ye de yazıp fonksiyonun dönüş değerine istenen bir adres yazılacak biçimde de çağırılabilir. Klasik arabellek taşırma saldırısı bu yolla yapılmaktadır. Bu sayede kodun akışı değiştirilebileceği gibi saldırgan tarafından yazılmış bir kod da(shellcode) çalıştırılabilir. Şekil 3'te arabellek ve BP 'A' harfiyle doldurulmuş dönüş değerine ise istenen bir değer yazılmıştır.
Şekil 3 İstismar edilmiş bir yığının görünümü
Programcıların Yapması Gerekenler
Bellek yönetimi
Arabellek taşması kısaca, değişkenlere varsayılandan büyük veriler girerek taşmaya sebep olunması ve bu sayede istenen bir kodun çalıştırılması olarak tanımlanabilir. Bu zafiyet Assembly, C ve C++ kullanarak yazılmış programlarda bulunmaktadır. Üst seviye programlama dillerinde runtime sırasında gerekli kontroller yapılmaktadır. Arabellek taşmasını anlayabilmek için öncelikle işletim sistemlerinin bellek yönetim sistemini anlamak gerekir.
Windows'ta tüm prosesler kendisine ait bir sanal adres uzayında bulunur. Runtime sırasında sanal bellek uzayındaki verilerle gerçek bellektekiler birbiriyle eşleştirilir. Bu sayede hiç bir proses bir başkasının alanına erişemez. 32-bit sistemlerde her bir proses için ayrılan sanal bellek alanı 4 GB'tır. Aynı alan 64-bit sistemlerde yaklaşık olarak 8 GB'tır. 32-bit sistemlerde bu alanın 0x00000000 ile 0x80000000 arasındaki bölümü proseslerin kendi bellek işlemlerini yaptıkları user space olarak kullanılırken, 0x80000000 ile 0xffffffff arasındaki bölümünü ise işletim sistemi kullanır ve bu alan kernel space olarak adlandırılır. Kernel space ancak işletim sistemi tarafından belirlenen fonksiyonlar aracılığıyla ulaşılabilir bir alandır.
Prosesler belleğe küçük bölümlere ayrılarak yüklenir ama genel olarak beş ana bölümden bahsedilebilir. Bunlardan text segmenti, program kodlarının assembly olarak bulunduğu yerdir. Text segmenti yazılabilir değildir. Bu segmente yazılmaya çalışıldığında program kapanır. Bu sayede kodun değiştirilmesi engellenmiş olur. Ayrıca text segmenti yazılabilir olmadığı için ihtiyaç duyduğu alan değişmez, dolayısıyla büyüklüğü sabittir. Data segmentinde değer atanmış değişkenler, bss segmentinde ise değer atanmamış değişkenler yer almaktadır. Örneğin; int x şeklinde oluşturulmuş değişkenler bss'te yer alırken, int x=0 ya da printf("yazi") gibi komutlarda yer alan değerler data segmentindedir. Bu segmentler yazılabilir oldukları halde boyutları değiştirilemez. Heap, bellekte runtime sırasında malloc, free gibi fonksiyonlarla işlem yapılmak istendiğinde kullanılır. Bu alanın boyutu değişkendir. Runtime esnasında kapladığı alan büyüyebilir veya küçülebilir. Heap segmenti yüksek belleğe, yani yığına doğru büyür.
Yığın (Stack)
İşletim sisteminde yeni bir thread oluşturulduğunda bellekte; fonksiyon parametrelerini, lokal değişkenleri ve fonksiyonların çalışması sonlandıktan sonra devam edeceği yeri saklamak için, yığın denilen alanlar oluşturulur. Her thread için iki yığın oluşturulur. Yığın aynı zamanda son giren ilk çıkar(LIFO - Last In First Out) prensibiyle çalışan soyut bir veri yapısıdır ve konumuz olan yığınlar da ismini paylaştığı bu veri yapısının prensibiyle çalışır.
Yığın da heap gibi değişken boyuta sahiptir. Küçük bellek adreslerine doğru büyüyen yığının en üstüne(düşük adrese) push komutuyla veri eklenirken, pop komutuyla son eklenen eleman çıkarılır.
Yığında en son eklenmiş verinin adresi SP(Stack Pointer) register'ında tutulur. Yani yeni bir veri eklendiğinde(push) veya eklenen son eleman çıkarıldığında(pop) SP'de tutulan adres değişir.
Arabellek taşması
Bir programda fonksiyon çağrıldığında, ilk önce fonksiyon parametreleri yığına yazılır. Ardından fonksiyondan çıkıldıktan sonra hangi komut çalıştırılacaksa, onun adresi yazılır. Sonrasında fonksiyona girmeden önceki değerini saklamak için BP'nin(Base Pointer) o andaki değeri ve ardından da BP'ye SP'nin değeri yazılır(function epilogue). Son olarak fonksiyondaki lokal değişkenleri tutmak için bir arabellek(buffer) alanı oluşturulur. Klasik arabellek taşması bu alanın taşırılarak IP'ye istenen bir değerin yazılması ve böylece program akışının değiştirilmesi durumudur.
Aşağıdaki fonksiyonda girilen parametreyle ilgili hiç bir denetleme bulunmamaktadır. Fonksiyon programcının beklediği biçimde, yani parametre olarak en fazla sekiz karakterlik bir dizi verilerek çağırılırsa yığın Şekil 2'deki gibi olacaktır.
Kod:
**** fonksiyon(char* parametre)
{
char arabellek[8];
strcpy(arabellek,parametre);
}
Ancak bu fonksiyon; arabelleği tamamen doldurup, ardından eski BP'ye de yazıp fonksiyonun dönüş değerine istenen bir adres yazılacak biçimde de çağırılabilir. Klasik arabellek taşırma saldırısı bu yolla yapılmaktadır. Bu sayede kodun akışı değiştirilebileceği gibi saldırgan tarafından yazılmış bir kod da(shellcode) çalıştırılabilir. Şekil 3'te arabellek ve BP 'A' harfiyle doldurulmuş dönüş değerine ise istenen bir değer yazılmıştır.
Programcıların Yapması Gerekenler
- Verilen örnekte sctrcpy yerine strlcpy fonksiyonu kullanılsaydı, geçilen parametrenin boyutu kontrol edileceği için zafiyet oluşmayacaktı. Aşağıda bazı fonksiyonların güvenli ve güvensiz versiyonları bulunmaktadır.
- Yazılan fonksiyonlarda girdi denetimi yapılmalı. Girdi denetiminde beyaz liste kullanılmalı ve listenin dışında kalan tüm girdiler reddedilmelidir.
- Kod yazılırken değişken büyüklüğünü her seferinde sayıyla girmek yerine bir kereliğine bir değişkene ya da sabite atayıp sonra hep o kullanılmalı veya değişkene yazılabilecek karakter miktarı sizeof fonksiyonuyla hesaplanmalıdır.
- Eğer aksi gerekli değilse Assembly, C ve C++ yerine, bellek yönetimi ve bellek taşması kontrolü yapan C# ve Java gibi üst seviyeli dillerle programlama yapmak tercih edilmelidir.
- Arabellek taşması güvenliği sağlayan kütüphane ve frameworkler kullanılmalıdır.
- Kod derlenirken derleyicilerin sağladığı arabellek taşması tespit mekanizmaları aktif hale getirilmelidir.
- İstemci tarafında yapılan kontroller sunucuda da yapılmalıdır.
- ASLR(Address Space Layout Randomization) ve DEP(Data Execution Protection) gibi özellikler kullanılmalıdır.
- Uygulamalara gereğinden fazla yetki verilmemelidir.
