İnat ve ısrarla devam diyenler için bu yazıda, Assembly programımıza biraz renk (!) katarak yolumuza devam ediyoruz.Tekrar etmekte fayda var,bu serinin amacı okuyucuları Assembly programları yazar hale getirmek değil, sayın okuyucular.Hedef azıcık ucundan crack, fazlasıyla hack amaçlı tersine mühendislik yapabilecek seviyeye getirmektir.Tamamdır ben doğru yerdeyim diyenlere, konularımız gelsin:
Lise yıllarında tanıştığımız,matematiğin göz nuru fonksiyon belası bugün yine karşımızda duruyor. Bakmayın siz bela dediğime, programlama dillerinindeki en basit konu olan fonksiyonları kısaca öğrenip asıl(!) belalara doğru tam gaz gidiyoruz.
Diyelim ki elimizde 435487643583 tane sayı var ve sıfırdan büyük olanları öğrenmek istiyoruz.Assembly dilinde karşılaştırma için cmp komutunu kullanıyoruz.Böyle bir durumda her karşılaştırma için tek tek cmp komutunu yazmak yerine istenilen_isim'de bir fonksiyon oluşturup ihtiyacımız olduğunda call komutu ile fonksiyonumuzu çağırıp işlemi yaptırabiliyoruz.
Fonksiyonlara isim verilirken genellikle başına _ karakteri konmasının sebebi program içerisinde oluşabilecek karışıklıkları baştan engelleme amacını taşımaktadır.Onbinlerce satırlık bir programda ufak bir karışıklık,sizi saç baş yolduracak hale getirebilir.
Fonksiyonlar genellikle şu komutlarla başlar ve biter:
_kompeer:
push ebp
mov ebp,esp
;diğer komutlar
mov esp, ebp ;bu ve
pop ebp ;bu satır yerine sadece leave komutu da verilebilir
ret
Yeni tanıştığımız push ve pop komutlarını stack kısmında anlatacağımız için şimdilik es geçiyoruz. Son komut olan ret yani return, girdiğimiz fonksiyonda işlemin bittiğini ve çağrılan noktaya geri dönmemizi sağlıyor. Peki çağrılan noktaya geri gelirsek tekrar fonksiyona girmez miyiz?Program saçma bir kısır döngüye girmiş olmaz mı?Demek ki döneceği adresi biliyor.Ama nasıl?
call komutu ile bir fonksiyona girildiği an,program döneceği adresi stack kısmına kaydediyor.Bu arkadaş sürekli karşımıza çıktığına göre önemli olsa gerek, hem de nasıl! Fonksiyonlarla tanıştığımıza göre muhabbeti koyulaştırmak için bir sonraki başlığımıza geçelim.
Programlama dillerinde sıkça kullanılan karşılaştırmalar neticesinde programdan farklı şeyler yapmasını isteriz.Mesela programın kurulumunda lisans anahtarını doğru yazana teşekkür et,yanlış yazana küfret gibi.Karşılaştırma kısmını öğrendiğimize göre şimdi de sonrasına bir göz atalım.
Eğer herhangi bir koşul söz konusu değilse,yani direk şuraya git demek için jmp komutunu kullanıyoruz.Peki koşul söz konusu olduğunda ne kullanıyoruz? Yok kısa mesafe atla, yok yakına atla 40 küsür tane şartlı atlama komutu mevcuttur.Örnek olarak jg komutuna bir bakalım.Jump If Greater, yani büyükse atla komutu cmp işlemi sonucunda EFLAG'lerden ZeroFlag ve SignFlag durumuna bakar ve ona göre ya teşekkür eder ya da küfür.
Şimdi de fonksiyonumuzu zıplar hale getirelim:
_kompeer:
push ebp
mov ebp,esp
.burayazipla:
cmp 0x1, 0x0
jg .burayazipla
mov esp, ebp
pop ebp
ret
cmp 0x1, 0x0 komutunda işlemci çıkarma işlemi (karşılaştırmayı böyle yapıyor) sonrası sıfır sonucu alamadığı için ZF açılmayacak yani set edilmeyeceği için program burayazipla kısmına atlayacak,işlem de sürekli tekrarlanacağı için programı 10 numara bir kısır döngünün içine sokmuş olduk.Görüldüğü üzere fonksiyon içerisinde atlama noktaları belirtmek için başına nokta koyup sonuna ikinokta koymanız yeterli oluyor.(Farklı derleyicilerde farklı durumlar söz konusu)
Bütün koşullu atlama komutlarını görmek ve detaylarını öğrenmek isterseniz,bu adreste listenin tamamı mevcut.Bu kadar zıplamadan sonra sıra harbi bela stack kısmına gelmiş bulunuyoruz.
İngilizce dersinde hoca "evladım,stack kelimesini bir cümle içerisinde kullan" dese, kuracağım ilk cümle herhalde "I do not know what stack is" olurdu.Nereden başlasam,nasıl anlatsam durumunun bariz örneğidir bu arkadaş.Kendileri geçici bilgilerin depolandığı tercüme edersek yığın hafıza olarak adlandırılan bir hafıza alanıdır.Bu arkadaşı bela yapan hapisten yeni çıkan babası değil,giriş çıkışlarda kullandığı Son-Giren-İlk-Çıkar (LIFO, Last In First Out) yöntemidir.
Başlarda neyin nesiymiş dedirten bu yöntemi anlatmak için enteresan bir betimleme deneyelim.Hafızanın bir kısmını oluşturan stack bölümünü ters duran bir çöp kutusu olarak düşünün.Bu çöplüğe her seferinde tek bir çöp atabiliyoruz.Çöp atmak için kullandığımız komut push.Peki herhangi bir çöpü tekrar geri almak istersek o zaman da pop komutunu kullanıyoruz. SGİÇ yönteminden ötürü çöp çıkarmak istediğinizde en son hangi çöpü attıysanız onu alabiliyorsunuz.
İlk yazıda öğrendiğimiz ESP yani stack pointer size son çöpün adresini gösterir.Bu arkadaşı baş belası yapan diğer bir akrabası ise, siz çöp attıkça miktar artmasına rağmen ESP size daha küçük bir adres gösterir.Çünkü stack alt adreslere doğru büyür.Hemen debuggar'dan kontrol edelim:
(gdb) print /x $esp (Espyi sorguluyoruz)
$1 = 0xbffffa70
(Push komutu ile bir çöp attık)
(gdb) print /x $esp (Tekrar soralım)
$2 = 0xbffffa6c
Görüldüğü üzere çöp miktarını arttırmamıza rağmen ESP adresi 4 byte (32 bit işlem sistemi) azaldı.Tekrar tekrar okuyun,kafanızda canlandırın,bu mevzuyu çözün derin.Lami cimi yok,öğrenilecek!
Bela bu kadarla bitse yine iyi.Gelelim fonksiyonları anlattığımız kısımdaki komutlara.Push ve pop komutlarını öğrendik,peki fonksiyon girişindeki push ebp + mov ebp,esp komutlarına.İlk komutla EBP registerındaki değeri geçiçi olarak stack çöplüğüne gönderiyoruz.Değişikliğe uğramadan önce geçiçi olarak stack çöplüğüne attık.Sonraki komutta ise ESP registerını EBP registerına kopyalıyoruz.Peki ama niye?
Fonksiyona girmeden önce stack çöplüğüne attığımız çöpleri kolayca çağırabilmek için ESP'nin mevcut adresini EBP registerına yazıyoruz. Sonra fonksiyon içerisinde ESPye türlü türlü çöpler atsak da EBP registerını baz alarak fonksiyona girişten önceki çöpleri rahatça isteyebiliyoruz. Yazının sonunda videoyu izlerken bu konuyu çok daha kolay anlayabilirsiniz.
mov esp, ebp + pop ebp komutları ile de başta yaptığımız işlemin tersini uygulayarak fonksiyon içerisinde atılan çöplerden tamamen kurtulmuş oluyoruz.Teorikte anlaşılması zor olan bu durum,ufak bir pratikle çok kolay çözülebilir.Bunun haricinde ret komutu ile fonksiyondan sonra hangi adrese nasıl dönüleceğini sorunun cevabı da yine stack çöplüğünde yatmaktadır. call komutu ile fonksiyona girdiğiniz anda program geri dönülecek adresi stack çöplüğüne atmaktadır.Çıkış vakti geldiğinde adresi stackten alıp kuzu kuzu programı kaldığı yerden devam ettirir.
Hack konusunda detayına ineceğimiz bir mevzudan kısaca bahsedelim.Başkasının yazdığı ve sizin kullanıcısı olduğunuz bir programa müdahele etmenin yolu, programı girdiği bir fonksiyondan geri,yazılan adrese değil sizin istediğiniz başka bir adrese yönlendirmektir.Bunu başarmak için de stackte bulunan ret adresini değiştirebilmeniz gerekiyor.Detaylar yeri ve zamanı gelince diyor,bir sonraki başlığımıza geçiyoruz.
Geçenlerde ünlü sanatçımız Küçük Emrah kendisine artık Emrah Erdoğan denmesini istedi.Kendisine küçük denmesinden rahatsız olmayan Küçük Endian yazımızın son belası olarak karşımıza çıkıyor. Orjinali Little Endian olan bu acıların çocuuu, önemli byteların (VIP oluyor bu arkadaşlar) sağa yazılması demektir.Hiç bi cacık anlamadıysanız,çok normal devam edelim.
Nereden çıktı,kimin aklına geldi bilinmez, işlemcilerde önemli byte'ı en sağa mı en sola mı yazacağını belirleyen bir sistem mevcut.Şu önemli kısmına bir örnek verelim.Mesela 1000 sayısında baştaki bir olmasa,geri kalan sıfırlar hikaye oluyor.Yani önemli byte olan bir sola yazılmış,tipik Big Endian örneği durumu.Intel işlemciler ki genelde hepimizin bilgisayarında bulunur, aynı sayısı 0001 şeklinde yazmayı tercih ediyor.Ne yazık ki kendisi Little Endian sistemini tercih etmiş.
Mevzumuza dönersek aynı durum hafızada bulunan bilgiler için de geçerli oluyor.Hemen debuggerdan kontrol edelim:
(gdb) i var (info variables komutunun kısa hali)
All defined variables:
Non-debugging symbols:
0x080490dc basla
(gdb) x/s &basla;
0x80490dc: "Cok pis karsilastiririm v0.1\n" (eee hani ters mers mevzuları,gayet düz bu ?!)
(gdb) x/xw &basla;
0x80490dc: 0x206b6f43 (Hex soracaksın abi!)
Hafıza adresini string sorguladığımız zaman debugger mevzuyu değiştirse de acı gerçek hex sorgulamada karşımıza çıkıyor. İkinci sorgulamada basla değişkeninin ilk dört byte'na baktığımızda 0x20 boşluk, 0x6b k harfi, 0x6f o harfi ve son olarak 0x43 C harfi karşımıza çıkıyor.Yani " koC sip srak sali irit ..." gibi birşey aslında.Umarım bu örnekle Little Endian sistemini çözmüşsünüzdür.
Teorik kısmı sıkıcı ve lanet,pratik kısmı eğlenceli ve sürükleyici olan bu konuları öğrendiğiniz an aslında ne kadar da basit olduklarını anlıyorsunuz.Bir sonraki Bremen Mızıkçıları başlıklı yazımızla Crack konusuna başlıyoruz. Bırkalayın,kurcalayın ama vazgeçmeyin!..
Yazı için hazırlanan videoyu YouTube'dan izleyebilirsiniz.
Videoda incelediğimiz programı indirip kendiniz de denemeler yapabilirsiniz.Kaynak kodu BURADA.