Bu başlık altında anlatacağım şeyler, “Bu ne ya?! Bir sürü saçma saçma sayı var!!”, diyebilecek
kişilere. Yani teknik bilgisi yeterli olmayan veya herhangi fikri olmayan kişilere burayı geçmemesini
şiddetle tavsiye ederim. Benim anlatacaklarını anlayacak kadar teknik bilgim var diyenler hiç zaman kaybetmeden 0. bölüme geçebilirleerrr.
HEEYYOOO! İlk olarak, “Bilgisayar bu kadar işlemi nerede ve nasıl yapıyor?” ve “Debugging
nedir?” sorularına cevap arayalım. İlk sorumuzun cevabı yani “nerede” kısmına cevap verecek olursak çok basit bir tanımla CPU diyebiliriz. Peki CPU dediğimiz şey bu kadar basit mi ifade edilir? Bir avuç içi kadar olan şey sizce ne kadar karmaşık olabilir? CPU’nun birçok bölümü vardır. Fakat bizi şu an “Registers” kısmı ilgilendiriyor. Registers dediğimiz bölüm adından da anlayabileceğimiz şekilde, bu bölümde kayıtlar tutulur. Peki ne/nelerin kayıtları tutulur? Nasıl tutulur? İşlemcimizin yaptığı, yapmış olduğu ve yapacağı tüm işlemler bu bölümde yer alır. Tabii ki bilgisayarımız çalışma mantığı gereği her şeyi sayılar ile tutar. Düşününce sizce de çok fazla karmaşıklık olmaz mıydı? Bu sorunun aynısı bizden yıllar önce soran devler bir yöntem geliştirmişler ve sayıları kümeler halinde adreslerde tutmuşlar. Vee bu adresler RAM olarak adlandırdığımız donanımda yer edinirler. İşte ilk sorumuzu kaba taslak bir şekilde cevaplamış oluyoruz. Kısa cevap ise: “Bilgisayar bu kadar işlemi CPU üzerinde gerçekleştirir ve ürettiği birçok sayıyı kümeler halinde adreslere yerleştirir. Ve bu adresler RAM’de bulunur”.
Debugging dediğimiz şey aslında bir çoğumuzun bildiği “Hata ayıklama” işlemidir. Birazdan da
göreceğiniz gibi kodumuzda yer alan hataları tespit etmek ve ayıklamak için bu yönteme başvuracağız.
“Eee soruları cevapladık ama hiç bağlantılı değil.” dediğinizi duyar gibiyim. Debugging
işlemini yaparken programımızın CPU üzerindeki adreslerine bakacağız ve hatalarımızı buralardan
cımbızla ayıklayacağız. O halde başlayalım mııı?

#0 Kodumuz ve Bir Çıtır Farkındalık

Kodumuz C dile yazılmış ve heap overflow zafiyetine sahiplik yapıyordur.

C bilmeyen kişilere için kodumuzu kısaca anlatmak isterim. 0-6. satırlar arasında bulunan yerde programımızın çalışması için gerekli olan fonksiyonların olduğu kütüphaneler bulunuyor. 24-40. satırlar arasında bulunan kodlar main bölümümüz. Program bu satırlar arasında yer alan kodlar ile çalışacaktır. Şimdi bu satırları tek tek analizleyim. 26. satırda data struct’ını d pointer’ı olarak çağırıyor. 27. satırda da bir önceki işlemde olduğu gibi fp struct’ını f pointer’ı olarak çağırıyor. Dahasında devreye malloc diye fonksiyon giriyor. Peki bu malloc fonksiyonu ne yapar? Kısaca anlatmak gerekirse malloc pointer’a sahip olan herhangi bir değişkene bellekte verdiğiniz yer kadar boşluk açar. Bu boşluklara ilerleyen yerlerde bakacağız. sizeof fonksiyonu verdiğiniz değişkenin boyutunu hesaplar. Yani 30 ve 31. satırlarda d ve f değişkenlerine, bellekte boyutları kadar yer açılıyor.

32. satırda f struct’ının içinde yer alan fp fonksiyonunu nowinner fonksiyouna eşitliyor. 36. satırda argv[1] değişkenini name değişkenine kopyalıyor. Ve hemen ardından nowinner fonksiyonunu çağırıyor. Ve bu şekilde programımız sonlanmış oluyor.

Peki winner fonksiyonuna ne oldu? Kodumuzun içerisinde olmasına rağmen hiç çağırılmıyor. Ve bizden istenen onu çağırmamız. Debugging bölümüne geçmeden önce hatayı nerede arayacağımızı belirtmek isterim. Debug etmeden önce 30-31 ve 36. satırlarda yer alan kodları iyice bir bakalım.

char tipi değişkenler 1 byte’lık yer kaplarlar. data struct’ının içinde bulunan name değişkeni 64 byte’lık veri saklayabilecek şekilde ayarlanmıştır. fp struct’ına bakıldığı zaman int tipinde fp fonksiyonu yer almaktadır.

Ve interger tipine sahip değişkenler 4 byte’lık yer kaplarlar. Bu nedenle d değişkeninde 64 byte’lık, f değişkeninde ise 4 byte’lık bir yer açılmıştır.

36. satırda yer alan kodta strcpy fonksiyonu çağrılıyor. Bu fonksiyon, d struct’ının içinde bulunan name değişkenine, bizim kontrol edebildiğimiz bir değişkeni kopyalamaktadır.

Buraya kadar anlattıklarımı anladığınızdan emin olduktan diğer bölüme geçebilirsiniz. Ve ufak bir soru: Hatayı fark edebildiniz mi? Biraz “sınır”ları zorlayın 🙂

#1 Debugging ve Final

VEEEEE işte birazdan sihir numaramızı gerçekleştireceğiz fakat sence de bir şey eksik değil mi? Evet! Neredeyse unutuyordum… İşte sihirli değneğin (gdb). Bu değnek senin işlerini kolaylaştırıyor ve basit düzey için anlaşılır kılıyor. O halde hemen başlayalıımmm.

Ve değneğimizi elimize aldığımıza göre debug etmeye başlayabiliriz. GDB bize ELF dosyamızı kolayca inceleme olanakları sağlıyor. Burada nasıl kullanıldığına değinmeyeceğim bu nedenle sizin araştırmanızı şiddetle tavsiye ederim. Size daha basit bir şekilde anlatabilmek için UI kullanacağım. Yani yukarıdaki görselde yer alan değneğin daha süslü versiyonu diyebiliriz 🙂

Programımızın nereden başlayacağınız belirleyebiliyoruz. Bizim hedefimiz winner fonksiyonunu çağırmak. Ve bu nedenle programda tüm işlemlerin yapıldığı ve yeriye sadece nowinner fonksiyonunu çağıracak olan yani 38. satıra break point’imizi koyuyoruz. Bu şekilde fonksiyon çağırmadan önce içeride neler olup olmadığını görmüş olacağız.

Break point’imizi koyduktan sonra programımızı çalıştırıyoruz. Ve girmiş olduğumuz ‘A’ input’unun bizim için ayrı özelliği var. Bellekte tutulan tüm bilgiler Hex sistemine göre tutulur ve bu sisteme göre A karakteri 0x41 değerini taşımaktadır. (Detaylar için ASCII tablosuna göz gezdirebilirsiniz.) Şimdi ise tüm işlemlerin tutulduğu adreslere bakalım.

Bizi ilgilendiren kısım heap kısmıdır. Bu kısımda malloc işleminde kullanılan adresler yer almaktadır.

Heap’in başlangıç adresine gittiğimiz zaman şöyle bir içerik ile karşılaşıyoruz:

Adreslerin içeriğine baktığımız zaman birçok şeyi rahatlıkla görebiliyoruz. nowinner fonksiyonunun adresini nereden öğrendiğimizi soracak olursanız, ilk olarak tecrübenizden kaynaklı bu değerin diğer herhangi bir değere benzemediğini anlayacaksınızdır. İkinci olarak ise fonksiyonları decompile edip adresilerini almak olacaktır. Decompile edilmiş fonksiyonlar aşağıda görüldüğü gibidir:

“Ne bu ya?”, dediğinizi duyar gibiyim. Decompile edilmiş kodlar Assembly dili formatında bize aktarılır. Çünkü bilgisayar kodları bu dilde anlar. Bu nedenle çıklarımız her daim Assembly dili formatında olacaktır. Karmaşlılığı açıklamak gerekirse çıktının sol kısmında yer alan değerler komutların tutulduğu adreslerdir. Fonksiyonun adresini öğrenmek için +0. adrese bakmamız yeterlidir. Bu şekilde hem nowinner fonkisiyonunun yerini tespit etmiş olduk hem de winner fonksiyonunun adresini öğrenmiş olduk.

İşşteee her buradan sonra başlıyor. Şimdi iyice en başından beri ne yaptıysak toparlayalım. İlk olarak kodumuzu analizledik ve bunun sonucunda bizden aldığı input’u bellekte 64 byte’lık yer açılmış d değişkenine kopyaladığını gördük. Peki dikkatinizi çeken bir şey oldu mu? Fark ettiğiniz üzere her ne kadar 64 byte’lık yer açılmış olsada bize verilen input’a veya kopyalanacak data’ya herhangi bir sınırlanma konulmamış. “Ne yani şimdi 100 byte’lık input girsek hepsini kopyalacak mı?” sorusuna hemen uygulamalı olarak cevap arayalım.

VEEE işte sorumuzun cevabı gördüğünüz üzere “EVET”. Program başlatılırken 100 adet ‘A’ girmesini istedik ve 100 adet ’A’ karakterini name değişkeninin içine kopyaladı. Peki ya nowinner’ın adresini ne oldu? Girmiş olduğumuz değerler adresin üzerine yazıldı ve bu nedenden dolayı adresin yerini bizim girmiş olduğumuz karakterler aldı. “Eee o zaman winner’ın adresini yazalıımm orayaaa.” dediğinizi duyar gibiyim. Tam olarak adresin olduğu yere denk gelen karakterleri hesaplamak için küçük bir çıkartma işlemine başvuruyoruz. 0x602060 – 0x602010 = 0x50 = 80. 0x602060 nowinner’ın tutulduğu adres, 0x602010 ise bizim input’umuzun başladığı adres. Yani 81. ve sonraki karakterlerimiz adresin üzerine yazılacaktır. Fakat adresi yazmadan önce küçük bir bilgi: Günümüz bilgisayarlarının çoğu little-endian mimarisine sahip işlemciler kullanılır. Little ve Big Endian’ı şöyle ifade edebiliriz;

Little-endian değerleri sağdan sola yazarak adresler. Big-endian ise bunun tam tersi işlemini yapar yani değerleri soldan sağa yazarak adresler. Bu nedenle adresimizi sağdan sola yazmalıyız.

YEEEYY!!! Final bölümüne gelmiş bulunuyoooruuzzz. Şimdi tek yapmamız gereken adresimizi tersten yazmak ve bunu input olarak vermek.

Adresimizi yazarken dikkat etmemiz gereken 2 özelliğimiz var; ilki az önce de bahsettiğim endian formatı, ikincisi ise byte formatında yazmak. Byte’ı ifade ederken “\x00” şekilinde yazıyoruz. winner fonksiyonunun adresi 0x400646. Tersten yazılmış hali 0x460640. Bunu byte tipine çevirecek olursak ortaya şöyle bir şey çıkar: \x46\x06\x40

Tüm sihirli sözcüklerimizz tamamlandııı!! Şimdi geriye sadece bu sözcükleri birleştirmek kaldı.

80 adet karakterden hemen sonra adresimizi yazıyoruz veeee…

5.0/5.0 Article rating
4 Reviews
Sizce bu yazı etkili miydi? Kendimizi daha çok geliştirebilmemiz için bize yardımcı olun!
  1. Wow!
  2. Mmm
  3. Hmm
  4. Meh
  5. Pff