Makale Özeti

Bir uygulamayı hatalardan ayıklamak için nasıl çalışmak gerekiyorsa güvenlik açıklarını sorun çıkarmadan ayıklamak için de çalışmak gerekir. Tabii ki en baştan tasarımın içinde güvenlikle ilgili unsurlara da yer verilmiş olmalıdır, ama yine de, gerekli tüm özen gösterilmişse bile, sık hata yapılan bazı noktalarda oluşabilecek güvenlik açıkları yeterince dikkate alınmamış olabilir.

Makale

Güvenlikle ilgili açıkların bulunması, normal hataların bulunmasından daha zordur. Çünkü normal hatalar, kendilerini normal koşullarda çalışırken, test edilirken büyük ölçüde belli ederler. Oysa güvenlik açıkları, çoğu zaman ancak özel ve zahmetli testlerle bulunabilirler. Bu açıdan, sıklıkla güvenlik açığı oluşan noktaları bilmek ve bunları kontrol etmek çok faydalı olacaktır.

Bu makalede bu noktalar üzerinde durulacaktır.

Makaleden en iyi faydayı sağlayabilmeniz için güvenlik, C# ve VB.NET konularında önbilginiz olması gerekmektedir.

Gerekli zaman ve çabayı ayırmak ya da ayırmamak

Kod inceleme zahmetli bir iştir, özellikle güvenlik açıklarını bulmak için yapılan incelemeler. Bu yüzden, yapacağınız incelemenin ne derinlikte olması gerektiğine önceden karar vermeniz iyi olacaktır. İnceleyeceğiniz kodun güvenlik bakımından derinlikli incelenmesi gerekip gerekmediğine karar vermek için aşağıdaki listeden kaç isabet aldığınıza bakmalısınız. Eğer bu listeden 3-4 isabet alan bir koda bakıyorsanız, detaylı bir incelemeye gereksinimiz var demektir:

  • Kod, varsayılan durumda çalışıyor mu?
  • Kod, yüksek ayrıcalıklarla mı çalışıyor?
  • Kod, bir network arayüzünde dinleme yapıyor mu?
  • Kullanılan arayüzde kimlik tanıtlaması yapılmıyor mu?
  • Kod C/C++ta mı yazılmış?
  • Kodun, daha önceden bilinen bir saldırıya açıklık tarihçesi var mı?
  • Güvenlik araştırmacılarının yoğun ilgisi altında olan bir kod mu?
  • Hassas ya da gizli veriler üzerine çalışan bir kod mu?
  • Yeniden kullanılabilir bir kod mu? (Bir dll gibi)
  • Yapılan tehlike analizlerinde yüksek riskte bir ortamda mı çalışıyor? Ya da yüksek riskte tehditlerle karşılaşan bir kod mu?

Güvenlik konusunda yaygın olarak sıkıntıya sebep olan konular kullanılan dile göre de değişiklik göstermektedir. Mesela C/C++ için birinci temel sorun kaynağı "buffer overrun"dır. Temelde hafızada ayrılan yerin sınırı aşarak kullanımını ifade eden bu güvenlik açığı o denli meşhur olmuştur ki, Türkçeye çevirmeden İngilizce adıyla kullanacağız bu makalemizde.

C# gibi daha yüksek katmanda çalışan dillerde buffer overrun üzerinde büyük ölçüde durulması gereken bir sorun değildir. Ama bu yüksek katmanda çalışan diller de genelde web uygulamalarında kullanıldıklarından bunlarda da başka türlü güvenlik sorunlarıyla karşılaşılmaktadır.

C ve C++da buffer overrun sorunu

Buffer overrun yazılım geliştirme endüstrisinin başındaki bir bela olarak tanımlanabilir. Alt katmanlarda çalışan dillerde, hafızada kullanılan yerin sınırının kontrolünde yapılan hatalardan kaynaklanan buffer overrun uygulamanıza değişik seviyelerde zarar verebilir.

Hafızaya yazılan bir değişken, kendi sınırları içinde kalmazsa komşusunun alanına girecektir. Basitleştirerek bu şekilde ifade edebileceğimiz sorun uygulamanıza üç değişik seviyede zarar verebilir.

Bunlardan biri, uygulamanızın akışında önemli sorunlara sebep olup çalışmasına engel olmasıdır. Bu, verebileceği zararların en hafifidir. Daha kötü bir zararı, uygulamanızın normal akışını bozmaması, ancak kimi değerlerde sapmalara sebep olarak bir çeşit mantık hatası oluşturmasıdır. Bu durumda uygulamanız ne yapacağı belirsiz bir serseri kurşun durumuna gelir.

Ama yine de verebileceği üçüncü tür zarar söz konusu olduğunda yukarıdaki zararlar masum kalmaktadır: Buffer overrun uygulamanızı bir suikast silahı haline getirebilir. Eğer oluşan güvenlik açığını kötüye kullanan kişi yeterince bilgili ve deneyimliyse, açık ve uygulamanızın genel yapısı konusunda gerekli bilgilere ulaşabildiyse, buffer overrunla mesela metodların dönüş adreslerinin üzerine yazarak kontrolü tamamen ele alabilir. Bu durumda uygulamanın çalıştığı bilgisayarda istediğini yapma şansına ulaşabilir.

Peki buffer overrunları tespit etmek için ne yapmak gerekir? En iyi çözüm bunları tespit etmek yerine oluşmalarına engel olmaktır. Ama bu daha iyi yöntem, bu makalemizin konusu değil. Şu anda yazılmış bir kodun incelenmesinden bahsediyoruz.

Kullanabileceğimiz yöntemlerden biri, uygulamanızdaki tüm giriş noktalarını tespit etmekle başlar. Uygulamanıza veri girişi olan tüm noktaları tespit eder, bu verilerin uygulama içinde değişikliğe uğradığı noktalar boyunca veriyi takip edersiniz. Güvenlikle ilgili temel ilkelerden biri, masum olduğu ispatlanana kadar tüm girdileri suçlu olarak kabul etmektir. Veri girdileriyle ilgili olarak düşünmeniz gereken şudur: Acaba bu verinin herhangi bir versiyonu kodun hatalı çalışmasına sebep olabilir mi?

Tipik bir durum, ascii karakter bekleyen bir değişkene unicode data verilmesidir. 4 ascii karakter istenirken 4 unicode karakter geldiğini düşünün. Bu durumda 4 byte olarak ayrılan alanınıza 8 byte gelmiş demektir. Yüksek katmanda çalışan dillerde bu tür durumlar zaten yönetilir durumdadır. Ama aşağı katmanlarda çalışan C, C++ gibi dillerde bu tür durumların kontrolünün yapıldığından emin olmak gerekmektedir.

Kullanılabilecek bir diğer yöntem, potansiyel tehlike taşıyan bilinen yapıları araştırmak ve bunlara gelen verileri geriye, giriş noktasına doğru kontrol etmektir. Bunlara bir örnek:

void fuction(char *p) {
    char buff[16];
    •••

    strcpy(buff,p);

    •••
}

Burada strcpy fonksiyonu bir tehlike oluşturmaktadır. Ama tehlike, fonksiyonun kendisinde değil kullandığı datadadır. p değişkenini giriş noktasına kadar geriye doğru takip etmek ve uygun yapıda olduğunun kontrol edilip edilmediğine bakmak gerekir.

Buffer overrun sorunu yüksek katmanda ve yönetilen ortamlarda çalışan dillerde büyük bir sorun oluşturmadığından, bu konuyu şimdilik burada kesiyoruz. Ama buffer overruna da sebep olabilecek bir başka noktaya dikkat etmemiz gerekiyor: Tamsayı taşması.

C ve C++ta tamsayı taşması

Çok özet olarak belirtecek olursak:

Eğer bir karşılaştırmada kullanılan matematiksel bir işlem yapıyorsanız, alt sınır ya da üst sınırdan taşma riskine sahipsiniz demektir. Bu karşılaştırma bir buffer alanının büyüklüğünü belirlemek için kullanılıyorsa ve kodunuzla ilgili bu durum kötü niyetli kişiler tarafından bilinir hale geldiyse başınız belada demektir.

Şu örneğe bir göz atalım:

void func(char *b1, size_t c1, char *b2, size_t c2) {
    const size_t MAX = 48;
    if (c1 + c2 > MAX) return;
    char *pBuff = new char[MAX];
    memcpy(buff,b1,c1);
    memcpy(buff+c1,b2,c2);
}

İlk bakışta herşey normal gibi gözüküyor. Ama daha dikkatli bakmak gerekli. size_t tipinden tanımladığımız MAXı 48 olarak sabit belirledik. Fonksiyonun içinde eğer c1+c2 toplamı 48den büyükse işlem yapmadan fonksiyondan çıkmak niyetindeyiz. Ama 0xFFFFFFF0 ve 0x40ın toplamı 0x30 (yani 48) ediyor. Yani son derece masum görünen fonksiyonumuz c1 ve c2ye bu değerlerin verilmesi durumunda 48 byte olarak sınırladığımızı sandığımız bir buffera yaklaşık 4GB kopyalıyor. Bingo, nurtopu gibi bir buffer overrunınız oldu.

Veritabanı erişimi

Bu konu daha çok C# gibi üst seviye dillerde önemlidir. C gibi dillerde pek veritabanı erişim kodu yazılmaz, ama yazıldığında burada anlatacaklarımız yine dikkate alınmalıdır.

Bakılacak ilk şey, parolayı içinde barındıran ya da admin hesaplarını kullanan bağlantı ifadeleridir. İkinci nokta ise, kodunuzun sql aşılamalarına açık olup olmadığıdır.

Eğer yönetilen kod (managed code) üzerinde inceleme yapıyorsanız, öncelikle System.Data namespaceini, özellikle de System.Data.SqlClientın kullanımını arayın. Bağlantı ifadesinin dikkat etmeniz gereken iki önemli özelliği vardır: User Id ve Password.

Şöyle bir şey gördüğünüzü varsayın:

DRIVER={SQL Server};SERVER=hrserver;UID=sa;PWD=$esame

Burada iki sorun var. İlki sa hesabının kullanılmış olması. Veritabanına programlarınız genellikle veri okumak, veri girmek ve veri güncellemek için bağlanır. Çok nadir olarak veritabanı yönetim fonksiyonlarıyla ilgili kod yazmanız gerekir. Hiçbir durumda genel kullanım için yazdığınız bir programda admin hesaplarını kullanmamalısınız. Aksi taktirde, kötü niyetli kişilerin işini çok kolaylaştırmış olursunuz. İkinci sorun ise, parolanın bağlantı ifadesinin içine gömülmüş olması. Böylece hem parolanızı keşfedilebileceği bir yere koymuş oluyorsunuz hem de parolada değişiklik yapılması gerektiğinde kodunuzda parolanın gömülü olduğu tüm yerlerde de bu değişikliğin yapılması gerekiyor.

Gelelim SQL aşılamalarına.

SQL aşılaması yapılabilmesini sağlayan temel faktör, kodunuzda metin birleştirmeye dayalı SQL ifadeleri oluşturuyor olmanızdır. Bu tür durum olup olmadığını kontrol etmek için kodunuz içinde "update", "select", "insert", "exec" ve kullanıldığını bildiğiniz database ya da tablo isimlerini aratmanız hızlı sonuca ulaşmakta yararlı olacaktır. Metin birleştirmeye dayalı SQL ifadelerinizi parametreli sorgulara dönüştürün. Parametrik kullanım durumunda SQL aşılamanın önüne geçmiş olursunuz.

Burada yanlış bir algı, stored procedure kullanımının yeterli olduğunun düşünülmesidir. Oysa, stored procedureü çağırırken metin birleştirmesi kullanmaya devam ediyorsanız, tehlikeye yine açıksınız!

Metin birleştirme "out"!

Web Sayfası Kodu

Web tabanlı uygulamalarda en sık rastlanan sorunlardan biri "cross-site scripting" (XSS) olarak ortaya çıkar. Bu işin temelinde, sitenizden bir kullanıcıya görüntülenen sayfada sizin istemediğiniz ve güvenilir olmayan kişilerden gelen şeyler yer almasıdır. Bu durumda kullanıcıya veri gönderen kod yapılarına bakmak gerekir. Mesela ASP için bakılması gerekenler, Response.Write ifadeleri ve <%=%> etiketleridir. Bu ifadeleri yakalayınca içlerindeki dataların kaynakları kontrol edilmelidir. Bir form ya da querystring gibi kullanıcı kaynaklı ve kontrol edilmemiş bir data gönderiliyorsa, bir güvenlik açığı oluşmuş demektir. Basit bir örnek:

Hello,
<% Response.Write(Request.QueryString("Name")) %>

Görüldüğü gibi "Name" parametresi geçerli ve doğru yapılandırılmış olduğu kontrol edilmeden kullanıcıya geri gönderilmiştir.

Sırlar ve Kriptografi

Sık yapılan iki hata: Sırları koda gömmek ve kendi kriptografik algoritmalarınızı oluşturmak.

Reverse engineering diye bir şey sanırım duymuşsunuzdur. Koda gömdüğünüz sırlar asla güvenilir durumda değildir. Birazdan ikna olmamış olanlarınız varsa diye bir olay anlatacağım, ama önce daha basit olan ikinci durumu düşünelim: Kendi kriptografik algoritmalarınızı oluşturmayın. Kriptografik algoritmalar uzun çabalar sonucu üretilmektedir. Programcıya düşen, bunları doğru bir şekilde kullanmaktır. Kendi kriptografik algoritmanızı yazmakla uğraşmak, hazır bir veritabanı yapısı yerine kendi veritabanı mantığınızı oluşturup kullanmaktan pek de farklı değildir.

Şimdi gelelim kodun içine gömülen parolaların neden güvenli olamayacağına. Bir kart düşünün, üzerindeki çipinde bir şifre saklıyor. Bu şifreyi kartı okuyacak sisteme taktığınızda doğru olarak girerseniz gizli bir şeyin kapısını açmış oluyorsunuz. Diyelim ki kartınız çalındı, nolucak?

Bu durumda sisteme kartı takıp şifreyi tahmine çalışırsa kötü niyetli kişiler büyük olasılıkla duvara toslayacaklardır. 4 haneli bir şifre olsa 9999 adet olasılık bulunmaktadır. (0000ı dahil edersek, 10 bin.) Bu durumda 3 tahminde parolayı bulma olasılığı kabaca (tahmini kolaylaştıran birtakım parola varsayımlarının etkisini bir kenara bırakacak olursak) 3/9999 olur. 3333te 1. Yeterince küçük.

Ama ya şunu yaparlarsa: Kartınızdaki devrelerde gerekli bağlantıları sağladılar ve her değeri tek tek girip enerji tüketimini ölçüyorlar. Doğru parola girildiğinde kart daha az işlem yaptığından daha az enerji tüketecektir. Böylelikle sisteme hiç takmadan yeterince iyi bir sistemle kartınızdaki gömülü sırrı elde etmiş olurlar. Dikkat ederseniz burada reverse engineering gibi daha basit yöntemlerden bahsetmedik bile.

Kısaca: Yazılan sır sır değildir. Kodunuza gömdüğünüz sır eğer sadece web servisi falansa, bir ölçüde güvenlidir.

Sonuç

Baktığımız bu noktalar, güvenlikle ilgili tüm açıkları yakalamanızı sağlayamaz. Bunlar, daha basit sayılabilecek ama aynı zamanda daha sık karşılaşılan hatalardır. Daha karmaşık hatalar için çok daha detaylı çalışmanız gerekebilir. Hem en doğrusu, hataları sonradan ayıklamaktansa kodu en baştan doğru olarak yazmaktır. Bu yolda kod incelemeleri ve bunlardan öğrenilen sonuçlar büyük bir katkı sağlayabilir.

Son bir hatırlatma: Dikkat ettiyseniz, sorunların çok büyük bir kısmı, güvenilmemesi gerektiği halde güvenilen kod yüzünden çıkmaktadır.

Unutmayın: Aksi ispatlanana kadar tüm veri girişleri şüphelidir!

Mustafa Acungil