Buffer Overflow Nedir? (Bölüm 1)

Merhaba.

Bu yazının konusu başlıktan anlaşıldığı üzere buffer overflow. Konuyu 2 bölüme ayırdım. 1. bölümde buffer overflow nedir. Nasıl kullanılır, nasıl oluşur kısmını. 2. bölümde ise bu bilgiler ışığında buffer overflow kullanımı ve shellcode yazımı hakkında bilgiler olacak. Buffer overflow’u anlamsal olarak tampon taşması olarak çevirebiliriz. Buradaki tampon tek başına bir anlam ifade etmeyecektir. Anlamlandırılması için tampon denilen şeyin neyi ifade ettiğini anlamak gerek. Bilgisayar programları kodlanırken çoğu zaman geçici değişken depolama alanlarına ihtiyaç duyulur. Bu geçici bellek alanlar istisnalar hariç lokal değişken statüsündedir. Lokal değişken olmaları bu verilerin programın Stack adı verilen bellek bölgelerinde tutulması anlamına gelir ki esas problemi oluşturan kısım da buradan kaynaklanır. Programlar hangi dil ile yazılırsa yazılsın nihai olarak makine diline derlendiklerinden ve makine diline ait verileri tutma amaçlı kullanılan registerlar kısıtlı olduğundan sonradan lazım olacak verileri saklama ihtiyacı vardır. Bu sonradan kullanılmak üzere verilerin depolandığı alana Stack (Yığın) adı verilir. Mantıksal olarak Last-In-First-Out (Son giren ilk çıkar) mantığında çalışır. Yani stack’e koyduğunuz ilk elemanın çıkması için ondan sonra konulan bütün elemanların çıkması gerekir. Buffer overflow tür olarak Stack tabanlı olabildiği gibi Heap (Dinamik bellek blokları) üstünde de olabilir. Biz bu yazıda stack tabanlı buffer overflowu göreceğiz. Heap tabanlı buffer overflow mantık olarak aynı falak pratiği biraz daha değişkenlik gösterebilen şeklidir. Başka yazıda bu konu üzerinde de açıklama yapabilirim.

Buffer overflow konusunu iyi anlayabilmek için temel düzey veya biraz daha üzeri assembly bilgisinin olması gerekir. Çünkü konuyu kavramak buna bağlıdır.

Evvela işin temellerinden başlamamız iyi olur. Stack nasıl işler nasıl görünür önce buna bakalım. Günümüzde geçerliliği olan her programlama dili kodu fonksiyon veya alt rutinlere ayırmamıza izin verir. Stack’in en yoğun kullanıldığı yerlerden brii de fonksiyon çağırma, fonksiyona argüman geçme ve çağrılan fonksiyondan geri dönme işlemleridir. Örneğin şöyle basit bir fonksiyon olsun. Bu fonksiyonun hem C dili ile yazılmış hali, hem de karşısında x86 assembly karşılığını göreceksiniz.

kod1

Bu fonksiyon iki adet argüman alıyor. Yaptığı iş ise iki değeri toplayıp geri döndermek. Bizi ilgilendiren kısım esasında bu işi nasıl yaptığı. Anlaşılması için örneği basit tuttum.

main fonksiyonu programımızın giriş noktası. func isimli fonksiyon iki adet argüman alıyor demiştik. C kodu ile 1 ve 2 değerini fonksiyona yazdık. Yani yapacağı işlem neticesinde fonksiyonun bize 3 değerini döndermesi gerekiyor. Assembly çıktısına baktığımızda iki adet PUSH komutu görüyoruz. PUSH komutu demin bahsettiğimiz Stack’e bir değer itiyor. Bunun karşılığında değeri çekmek için POP komutu kullanılır. Yani önce parametreleri stack’e atıyoruz. Burada calling convention’a göre paramterlerin aktarım şekli ve sırası değişkenlik gösterebilir. Calling convention bir fonksiyonun nasıl yorumlanması gerektiğini bildiren işarettir. Burada default olarak cdecl kullanıldığından parametreler stacke’e sağdan sola sırasıyla atılır. Stack çağıran fonksiyon tarafından düzenlenir.  Hemen yukarıda ise toplama işlemini yapıp dönen fonksiyonu ve karşılığında assembly dökümünü görüyorsunuz. Fonksiyon öncelikle biraz evvel geldiği fonksiyona ait EBP değerini saklamakla işe başlıyor çünkü EBP az sonra kendine lazım olacak. Programın EBP’ registerini de stack’e atmasıyla stackin görüntüsü şuna benzer olacaktır.

diag1

Stack’in en tepesinde lokal değişkenlerin ayrıldığı bölüm bulunuyor. Lokal değişkenleri şimdilik düşünmeyin ona birazdan gelecez. Biraz evvel anlatırken program EBP’yi stacke atmıştı. Lokal değişkenlerin bulunduğu kısımdan sonra EBP sırayı alır. Önceki fonksiyonun kullandığı EBP değeridir. Hemen arkasından Dönüş adresi yer alır. Sonrasında da fonksiyona geçilen argümanlar (Bizim örneğimizde p1 ve p2). Peki burada herşey demin gördüğümüz gibi ama dönüş adresi nasıl araya girdi diyenler olabilir. Dönüş adresini nasıl aldık da stacke koyduk? Cevabı CALL komutunda yatıyor. Yukarıya tekrar dönüp bakarsanız parametreleri stacke attıktan sonra CALL ile fonksityonu çağırmıştık. CALL komutu biz görmesek de aslında şu işi yapıyor.

PUSH EIP
JMP FUNCTION

Yani EIP (Instruction Pointer)’I stacke koy ve fonksiyon adresine JMP komutu ile zıpla. Böylece EIP değeri de stackte yerini alıyor. Yine yukarıdaki assembly çıktısını hatırlayacak olursak değişkenlere erişirken EBP kullanıldığını görürüz. En başta stackin tepesini gösteren ESP registerini referans alıyoruz. Önce stack ile alakalı tüm işlemler yapılıyor daha sonra ilgili adresi değişken erişimi için referans alıyoruz. EBP bu amaçla kullanılır. Adı da (Base Pointer) buradan gelir. ESP ise stacke ilişkin olduğundan Stack Pointer adını alır. Başındaki E ise extenden anlamındadır. 32 bit register olduğunu belirtmek için kullanılır.

Eğer 32 bit bir makinede çalışıyorsak (ki örneğimiz 32 bit üzerine) bir register uzunluğu 32 bit yani 4 byte uzunluğundadır. Her veri ister 1 byte uzunluğunda olsun stacke atıldığında 4 byte yer işgal eder. Daha doğrusu etmek zorundadır.  Bu ufak bilgiyi öğrendikten sonra değişkenlere veya argümanlara erişimi anlamamız kolaylaşacaktır. Yukarıdaki diagramda p1 ve p2 değişkenlerine ESP esasında EBP çünkü esp ilerde değişebileceği için referansımızı kaybetmemiz olasıdır. O yüzxen EBP ile bu referansı garanti altına alıyoruz. Orası esasında EBP+8 ve EBP+C olarak düşünebilirsiniz. Birinci argümanımız olan p1’I EBP+8, ikinci argümanımız olan p2’yi EBP+C adresinden alabiliriz. Neden doğrudan EBP değil de +8 byte sonrası? Yukarıdaki diagrama bakarsanınız argümanların üstünde eski EBP ve EIP değerlerinin olduğunu görürsünüz. Her bir verinin 4 byte uzunluğunda olduğunu söylemiştim. EBP’nin kendisinde eski EBP değeri, EBP+4 de ise EIP bulunacağından bizim ilk argümanımmız almamız için EBP + 8 adresinden başlamamız gerekecek. sonrasında + 12 byte adresinden de 2. argümanımız alınacak. Oradaki C bir heksadesimal sayıdır. Matematik dersinde 16 tabanında sayılar olarak görmüş olmanuz lazım Smile C değeri 10luk tabanda 12 sayısına karşılık gelir. Bilgisayarlarda byte değerlerinin 16 tabanında tutulması ve manipüle edilmesi yaygındır.

Buraya kadar normal bir fonksiyonun nasıl çalıştığını, stack ile olan ilişkisini stackte nasıl göründüğünü gördük. Şimdi buna bir lokal buffer değişkeni ekleyip inceleyelim. Konumuz Buffer Overflow ya artık içine girmesi lazım Smile

Aşağıdaki kod ve assembly karşılığı yukarıkdai örneğe buffer lokal değişkeni eklenmiş hali. C kodu açısından farklılık çok az.

kod2

2. kod bloğunun işleyişi yukarıkdai ile tamamen aynı. Orayı tekrar anlatmıyorum. Bizi alakadar eden kısım üstündeki fonksiyon bloğu. En baştaki koddan farkını görmüş olmanız lazım. Görnediyseniz kırmızı dikdörtgenle çevrili kısma dikkat edin. Orada 5 byte uzunluğunda bir buffer tanımladık. C dili biliyorsanız char değişken tipinin 1 byte yer işgal ettiğini bilmeniz lazım. Bilmiyorsanız C bilginizi tekrar gözden geçirin. Biz orada arka arkaya 5 tane bytedan oluşan bir dizi oluşturmasını istedik. C kısmında olay gayet açık. Gelelim assembly dökümüne. Yine kırmızı dikdörtgen içinde bir assembly komut dizisi görüyorsunuz. Bu bufferimizi için yer açmakta kullanılıyor. Basitçe stack pointeri 8 byte öteliyor. Peki neden 8 byte? Biz 5 byte istemiştik. Cevabı yine stack ve register uzunlukları, ve buna bağlı olarak yapılan hizalama (Alignment). Yani siz eğer 4 ve katlarına hizalı bir bellek miktarı vermediğiniz sürece bu otomatik olarak derleyici tarafından 4 ve katlarına hizalı biçimde ayarlanır. Yani biz 5 byte istedik. bunun 4 byte’ı bir blok olarak düşünürsek geriye 1 byte artıyor. Yani otomatik olarak 8 byte’a yuvarlanıyor. Eğer 11 byte isteseydik bu otomatik olarak 12 byte olarak yuvarlanacaktı. Buradaki yaklaşım derleyicilere göre değişkenlik gösterebilse de örneğin 5 byte uzunluğunda bir buffer ayırıp 1 veya 2 byte’da ayrı bir değişken için yer istediğinizde derleyici hizalamadan arta kalan belleği diğer lokal değişken için kullanabilir. Ama genelde yanlışlıklara mahal verebileceği için pek tercih edilmez. Tam olarak bu noktada EBP’nin üstlendiği işi daha iyi görüyoruz. Yukarda referans alınması gereken ESP değerini tutmamız gerek demiştim. Bu örnekte EBP kullanılmasaydı. Biz değişlenlere EBP+8 diyerek ulaşmaya devam etseydik SUB ESP,8 komutundan sonra ESP+8 adresi bize bambaşka bir değeri gösterecek ve programımız yanlış çalışacak belki de çökecekti. Yeni duruma göre Stack’in durumunu grafiksel olarak gösterirsek

diag2

Bu şekilde bir görüntü ile karşılaşırız. En üstte buffer değişkenimiz. Sonra eski EBP ve EIP. Şimdi bu noktada Buffer Overflow nedir sorusunu yanıtlayacağım.

Buffer Overflow!

En başta demiştim ki buffer overflow tampon belleğin taşması durumuna denilir. Peki taşarsa ne olur? Stack belleği ardışık bir bellek bloğu olduğundan buffer’iniz taştığında fazla gelen veri eski EBP üstüne yazılabilir mi? Evet yazılabilir. Hatta bir miktar daha taşırırsak bu EBP’yi de aşıp fonksiyonun geri dönüş değerini saklayan stack lokasyonunun üzerine de yazılabilir mi? Gayet tabi yazılabilir. Peki yazılırsa ne olur? Çalışan fonksiyon işini bitirip döneceği adrese baktığında beklediği adresten farklı bir adres görür ve oraya gider. İşte buffer overflow’u tehlikeli yapan nokta da burasıdır. Amaç da tam olarak EIP üzerine yazabilmektir. Bunun için buffer yeteri kadar miktar veri ile doldurulmaya çalışılır.

Örneğin en son verdiğim kod üzerinde ufak bir buffer overflow oluşturalım. Normalde kodu biliyoruz. Yani buffer’ı taşırıp EIP üstüne yazmaya yetecek veri miktarını önceden hesaplayabiliriz. Önceki açıklamaları hatırladığımızda demiştik ki 4 ve katlarına hizalı olmayan bufferlar otomati hizalanır. Yani biz 5 byte istedik bu otomatik 4’ün katı olan 8 byte’a hizalanacaktır. Elimizde var 8. 4 byte’ da EBP uzunluğu. 8 + 4 = Toplamda 12 byte gerekli. 12 byte junk (çöp) data sonrası EIP değeri olacak.

bofexam

Programı bir debugger yardımı ile çalıştırdığımızda (Ben ollydbg kullandım) access violation hatasını ve hangi adrese erişmeye çalıştığını görebiliriz. Burada EIP’in hex (16 tabanında) 0x42424242 değerini aldığını görebiliriz. Buradaki 42 ‘B’ harfinin ASCII tablosundaki hex değeridir. 12 adet A buffer ve EBP üstüne yazmakta kullanıldı. Dikkat ederseniz EBP’nin de ‘A’ harfinin hex karşılığı olan 41 değeri ile dolduğunu görebiliriz. Yani işler tam da beklediğimiz şekilde yürümüş.

Karakterlerin ASCII tablosunda karşılıkları için aşağıdaki tablodan yararlanabilirsiniz.

Chr sütununda karakter değerini bulup, Hx sütunundaki hex karşılığını alabilirsiniz.

Programımız access violation hatasına sebep oldu çünkü 0x42424242 adresinde geçerli bir kod verisi yok. Bizim buffer overflow saldırımızın başarılı olması için bu zıplama adresinin geçerli bir yere tekabül etmesi gerekiyor. Bu noktadan sonra saldırının başarılı olması buffer uzunluğuna çünkü genellikle bufferi dolduran şey sizin shellcode’unuz olacak. Sizin bu buffer’a gerekli shellcode’u oraya sığdırmanız gerekiyor. Peki buffer uzunluğunu bilmediğimiz durumda ne olacak. 1. cisi hedef uygulamanın ilgili sürüm kopyasını edinip decompile edip incelemek. Bu biraz zaman alıcı ve pek pratik değill. 2. yöntem deneme yöntemi. Bufferi belli patternlerde stringlerle doldurup access violation’a sebep olmak ve patterne denk düşen hex karakteriyle oluşmuş violation adresi tespit edip ne kadar buffer size lazım öğrenmek. Örneğin uygulama beklendiği gibi dışardan veri alıyor. Ama biz bilmiyoruz ne kadar buffer’a sahip taşırmak için ne kadar alan lazım. Belli patternlerle string gönderiyopruz. Ben ollydbg ile açıp komut satırı argümanına bir dizi pattern veriyorum.

det1

Pattern AAAABBBBCCCCDDDDEEEEFFFF. 4 er byte bloklarla farklı harf kombinasyonları veriyorum. Eğer taşma olursa başta gösterdiğim ASCII tablo yardımıyla EIP’e hangi kısım gelmiş ne kadar buffer lazım bize göreceğiz.

det2

0x45454545’te access violation oluştu. EIP’ üstüne bu data yazılmış. Bakıyoruz. 0x45 ‘E’ karakterine denk düşüyor. Buradan hareketle bize 16 byte uzunluğunda verinin bufferi taşırıp EIP üzerine yazmaya yeteceğini gösterdi. Smile

Konunun 1. bölümü burada bitiyor. Eğer amacınız sadece Buffer Overflow nedir öğrenmek idiyse bu bölüm size yetecektir. Eğer dahasını öğrenmek istiyorsanız 2. bölümü beklemeye başlayabilirsiniz. 2. bölümde Buffer Overflowun pratikte kullanımı, nasıl kullanıldığı, shellcode yazımı. vs bilgilerine yer vereceğim.

Herkese kolay gelsin.

7 thoughts on “Buffer Overflow Nedir? (Bölüm 1)

  1. ben de teşekkür ederim.

    @ilyas bana kalsa hemen yazıp yayınlamak isterim ancak şöyle güzel bir vakit bulup yazmaya fırsat bulamıyordum. yakın bir zamanda özellikle bunun için vakit ayırıp yayınlamayı düşünüyorum. çünkü parça parça yazmaktan ziyade bir seferde oturup komple bitirmeyi seviyorum konunun bütünlüğü açısından.

  2. @Onur, çalıp çırpmak bana göre bir şey değil. Bir iddian varsa onu ispatlamakla yükümlüsün. Makalenin sahibinin sen olduğunu buyur ispat et bakalım. Kusura bakma ama emek verip yazdığım bir yazıyı arsızca gelip kendine mal etmeye çalışıp bir de zeytinyağı gibi üste çıkmaya çalışıyor olmandan dolayı esas utanması gereken sensin. Benim utanacak bir şeyim yok. Daha Türkçe imla kurallarını düzgün yazamayıp imla kurallarına uygun yazılmış bir yazıyı ben yazdım demek de oldukça komik olmuş.

Leave a Reply

Your email address will not be published. Required fields are marked *