1988 yılındaki Morris solucanı, sektörü derinden etkileyen deneyimlerden biriydi. Bu olay, arabellek taşması veya arabellek aşımı olarak bilinen bir güvenlik açığını kullanarak bir solucanın ne kadar hızlı yayılabileceğini gösterdi. İnternetin öncüsü olan ARPANET’e bağlı 60.000 bilgisayardan neredeyse 6.000’i Morris solucanından etkilenmişti. Bu saldırıdan yazılım satıcılarının güvenlik açıklarını ciddiye alması ve ilk Bilgisayar Acil Durum Müdahale Ekiplerinin (CERT) kurulması gibi dersler çıkarılmış olmasına rağmen bellek taşmasından faydalanan son saldırı bu olmayacaktı.
2001 yılında Code Red solucanı, Microsoft’un IIS yazılımı kullanan 359.000’den fazla bilgisayarı etkiledi. Code Red, web sayfalarını tahrif etti ve Beyaz Saray’daki bir web sunucusu dahil olmak üzere hizmeti engelleme saldırıları düzenlemeye çalıştı.
Daha sonra 2003 yılında SQL Slammer solucanı, Microsoft’un SQL Server yazılımını kullanan 250.000’den fazla sisteme saldırdı. SQL Slammer, yönlendiricileri etkileyerek internetteki ağ trafiğinin fazlasıyla yavaşlamasına, hatta durma noktasına gelmesine sebep oldu. Code Red ve SQL Slammer solucanları, arabellek taşması güvenlik açıkları yoluyla yayıldı.
Morris solucanının üzerinden otuz yıldan fazla geçmesine halen birçok olumsuz sonuca neden olan arabellek taşması güvenlik açığıyla sıklıkla karşılaşıyoruz. Bu durumun sorumlusu olarak bazıları çeşitli programlama dillerini veya bu dillerin özelliklerini güvensiz bir tasarıma sahip olmakla suçlasa da görünen o ki asıl sorun bu dillerin yanlış şekilde kullanılmasında. Arabellek taşmasının nasıl gerçekleştiğini anlamak için yığın başta olmak üzere bellek hakkında ve yazılım geliştiricilerin kod yazarken belleği nasıl yönetmeleri gerektiği hakkında biraz bilgi sahibi olmalıyız.
Arabellek nedir ve arabellek taşması nasıl gerçekleşir?
Arabellek, işletim sisteminin yazılım programı için atadığı bir bellek bloğudur. Program, işletim sisteminden programın doğru şekilde çalışması için gereken bellek miktarını talep eder. Java, C#, Python, Go ve Rust gibi bazı programlama dillerinde bellek yönetimi otomatik olarak yapılır. C ve C++ gibi dillerde ise programcılar, belleğin atanmasını ve belleğin boşaltılmasını manuel olarak yapar ve arabellek uzunluklarını kontrol ederek bellek sınırlarının geçilmediğinden emin olur.
Ancak kod kitaplığını yanlış kullanan programcılar veya kodları yazanlar hata yapabilir. Bu hatalar, keşfetmeye ve suistimal edilmeye açık bir çok yazılım güvenlik açığının ortaya çıkmasına neden olur. Düzgün bir şekilde tasarlanmış bir program, veri bulundurmak için gereken maksimum bellek boyutunu belirlemeli ve bu boyutun aşılmamasını garanti etmelidir. Program, atanan bellekten daha ötesine ve başka bir kullanım için ayrılan veya başka süreçler için gerekli olan bitişiğindeki bellek bloğuna veri yazdığında arabellek taşması gerçekleşir.
Küme (heap) kaynaklı ve yığın (stack) kaynaklı olmak üzere, arabellek taşmasının iki ana türü vardır. Bu ifadeler küme ve yığın arasındaki farkı anlatmak için kullanılır.
Yığın ve küme karşılaştırması
Bir program yürütülmeden önce yükleyici o programa küme ve yığın adreslerini içeren bir sanal adres alanı atar. Küme, genel değişkenler ve çalışma süresinde belleğe atanan değişkenler (dinamik olarak ayrılan) için kullanılan bir bellek bloğudur.
Üst üste dizilmiş bir tabak yığınına benzeyen yazılım yığını ise çağrılan bir işlevin yerel değişkenlerini bulunduran çerçevelerden oluşur. İşlevler çağrıldığında çerçeveler yığına itilir (yığının üzerine eklenir) ve döndüklerinde yığından çıkarılır. Birden çok iş parçacığı olması durumunda, birçok yığın oluşur.
Kümeyle karşılaştırıldığında yığın çok hızlıdır ancak yığın kullanmanın iki tane olumsuz tarafı bulunur. İlk olarak yığın belleği sınırlıdır, yani daha hızlı bir şekilde büyük veri yapılarını yığına yerleştirmek mevcut adresleri yorar. İkinci olarak her çerçevenin kullanım ömrü yığın üzerinde yer almasıyla sınırlıdır, yani yığından çıkarılan bir çerçevedeki veriye erişim sağlamak mümkün değildir. Birden çok işlevin aynı veriye erişim sağlaması gerektiğinde veriyi kümeye yerleştirmek ve bu işlevlerin verilerine (adresine) imleç eklemek daha iyidir.
Arabellek taşması, kümede de yığında da gerçekleşebilir. Ancak bu yazıda daha sık karşılaşılan tür olan yığın kaynaklı arabellek taşmasına odaklanıyoruz.
Yığın kaynaklı arabellek taşması: Dönüş adresinin üstüne yazma
Her işlev çağrıldığında çerçeveler birbirinin üzerine yığıldığından dönüş adresleri de yığının üzerine biner ve çağrılan işlev tamamlandığında programa yürütmeye nereden devam edeceğini söyler:
Dönüş adresi, yerel değişkenleri bulunduran arabelleklerin yakınında yer alır. Bu nedenle kötü amaçlı bir programın bir arabelleğe bulundurabileceğinden daha fazla veri yazmada başarılı olması durumunda arabellek taşması gerçekleşir. Amaçlanan arabelleğe uymayan veri, dönüş adresine doğru ‘taşar’ ve dönüş adresinin üzerine yazılır.
Güvenlik açığına sahip bir programın tipik kullanımında bir arabellek taşması gerçekleşmesi durumunda genellikle üzerine yazılan dönüş adresinin yeni değeri geçerli bir bellek konumu değildir, yani program bellek ayırma hatası oluşturur ve hata kurtarma gerekir. Hata kurtarma mümkün olmadığında program istikrarsız hale gelebilir, hatta program taşma tarafından yığın çerçevesi değiştirilen işlevden dönmeye çalışırken çökebilir. Ancak siber suçlular arabellek taşmalarından faydalanır. Doğrudan kendi kötü amaçlı kodlarını gösteren geçerli bir bellek konumu ile dönüş adresinin üzerine yazarak birçok durumda kabuk oluşturabilir ve kurban bilgisayarların kontrolünü tamamen ele geçirebilirler. Örneğin Stuxnet solucanı bir kök kabuğu oluşturmak için arabellek taşması güvenlik açığını kullandı.
Bazı suistimal kodları, kötü amaçlı bir etkinlikten sonra akıllıca bir yaklaşımla yığındaki zararı tamir ederek özgün dönüş adresini kurtarır. Saldırganlar böylece dönüş komutu ihlalini gizlemeye çalışır ve sonrasında programın beklendiği şekilde çalışmaya devam etmesini sağlarlar.
Örnek - Onaltılı karakterleri bayt değerleri olarak kodlama
2021 yılında keşfedilen bir arabellek taşmasıyla ilgilenen yazılım geliştiriciler için C dilinde yazılan aşağıdaki kodu öneriyoruz. CVE-2021-21748 olarak izlenen ZTE MF971R LTE yönlendiricideki güvenlik açığının basitleştirilmiş ve yeniden yazılmış bir sürümüdür:
Yukarıda verilen program, onaltı basamakla uyumlu karakterlerden oluşan bir dizeyi yarı bellek gereksinimi ile bir biçime kodlayan işlevi gösterir. İki karakter, gerçek bayt değerleri (onaltı basamaklı) olarak bulunabilir, bu sayede sırasıyla 30 ve 31 bayt değerleri ile temsil edilen ‘0’ ve ‘1’ karakterleri, 01 bayt değeri olarak temsil edilebilir. Bu işlevsellik, ZTE yönlendiricinin parolaları ele almasının bir parçası olarak kullanıldı.
Kodun yorumlarında belirtildiği üzere 21 karakter boyutuna sahip hexString, yalnızca 4 karakter boyutuna sahip byteValues arabelleği için çok büyüktür (kodlanmış biçimde 8 karaktere kadar kabul edebilmesine rağmen) ve encodeHexAsByteValues işlevinin bir arabellek taşmasına neden olmayacağından emin olmanın bir yolu yoktur.
Arabellek taşması saldırılarından korunma
Dikkatli programlamalar ve testler geliştiren yazılım programlamacılarının yanı sıra modern derleyiciler ve işletim sistemleri, arabellek taşması saldırılarının gerçekleşmesini zorlaştırmak üzere çeşitli mekanizmalar uyguluyor. Linux’un GCC derleyici sürücüsünü örnek olarak ele alarak, arabellek taşmasının suistimal edilmesini engellemek için kullanılan iki mekanizmadan bahsedebiliriz: yığın rastgeleleştirme ve yığın bozulma tespiti.
Yığın rastgeleleştirme
Arabellek taşması saldırılarının başarısı, bir bakıma suistimal koduna işaret eden geçerli bellek konumunu bilmeye dayanıyor. Geçmişte yığın konumları, programların ve işletim sistemi sürümlerinin aynı kombinasyonları aynı yığın adresine sahip olduğundan genelde tek bir biçimdeydi. Bu sebeple saldırganlar, tek bir biyolojik virüs suşu gibi, aynı program-işletim sistemi kombinasyonuna saldırmak için tek saldırıyı yönetebiliyordu.
Yığın rastgeleleştirme, program yürütmenin başlangıcında yığına rastgele bir alan ayırıyor. Bu durum alanın program tarafından kullanılacağı anlamına gelmiyor ancak programın her yürütmede farklı yığın adreslerine sahip olmasını sağlıyor.
Ancak kararlı bir saldırgan tekrar tekrar farklı adresleri deneyerek yığın rastgeleleştirmenin üstesinden gelebilir. Suistimal kodunun başında yalnızca program sayacını artıran uzun bir NOP (operasyon yok) talimatı dizisi kullanmak tekniklerden biridir. Bu durumda suistimal kodunun başlangıcındaki tam adresi tahmin etmek yerine, saldırganın, birçok NOP talimatından herhangi birinin adresini tahmin etmesi yeterli olur. Program bu NOP talimatlarından birine atladığında suistimal kodunun gerçek başlangıcına kadar NOP talimatlarının kalanında kaymaya devam ettiğinden buna “NOP kızağı” denir. Örneği Morris solucanı, 400 NOP talimatıyla başladı.
Adres alanı düzeninin rastgeleleştirilmesi denilen birçok teknik bulunur. Bu tekniklerin amacı program kodu, kitaplık kodu, genel değişkenler ve küme verisi gibi bir programın diğer parçalarının program her çalıştığında farklı bellek adreslerine sahip olmasını sağlamaktır.
Yığın bozulma tespiti
Arabellek taşması saldırısını önlemenin bir diğer yöntemi de yığın bozulduğunda tespit etmektir. Yığın koruyucu olarak bilinen yaygın bir mekanizma, yığın çerçevesinin yerel arabellekleri ve yığının geri kalanının arasında rastgele bir kanarya değer veya diğer adıyla koruma değeri yerleştirir. Bu sayede bir işlevden dönmeden önce program kanarya değerin durumunu kontrol edebilir ve bir arabellek taşması, kanarya değeri değiştirdiyse hata rutini çağırabilir.
Son tavsiye
Arabellek taşması güvenlik açıkları keşfedilmeye ve düzeltilmeye devam ederken bu konudaki en iyi tavsiye tüm uygulamaları ve kod kitaplıklarını en yüksek öncelikle yamalamak üzere sağlam bir politikaya sahip olmaktır. Suistimal kodlarını tespit edebilen güvenlik çözümlerinin yanı sıra politikanın sürekli olarak güncel olmasını sağlayarak arabellek taşmalarını suistimal etmek isteyen saldırganlara karşı güçlü bir koruma oluşturulabilir.