BUFFER OVERFLOW
Buffer Overflow, RAM üzerindeki Buffer kısmının taşması ile ortaya çıkan bir güvenlik açığıdır.
Buffer Overflow saldırısını anlamak için önce biraz işlemcilerden ve RAM’lerden bahsedeceğim.
İşlemcileri daha önce donanımlar ile ilgili yazdığım yazıda detaylıca anlatmıştım, fakat bu yazımda güvenlik açısından bilmemiz gereken Register kısmına detaylı bakacağız.
Register’a , işlemcinin kendisine verilen görevleri yaptığı bir “karalama defteri” diyebiliriz. Register’da kendi içerisinde dörde ayrılır.
- Genel amaçlı register
- Durum bayrakları
- Segmentler
- Komut işaretçisi
Şimdi bu bölümleri tek tek inceleyelim.
GENEL AMAÇLI REGISTER(GPR)
Genel amaçlı registerlar kendi arasında ayrılır, bunları kısaca anlatacağım, Buffer Overflow saldırısı ile ilgili olanları ise detaylıca inceleyeceğiz.
NOT: Eğer sistem 64 bit ise bu registar’ların başına R yani Register eğer 32 bit ise E yani extended harfleri gelir. Bütün genel amaçlı registerlar Assembly dilinde bir değişken olarak kullanılırlar. Assembly konumuzla bağlantılı olsa da, bu makalede Assembly kodlarına girilmeyecektir.
RAX & EAX: A harfi akümülatör anlamına gelir ve genelde döndürülen işlemin sonucunu tutar. Assembly dilinde sistem çağrılarını tutmak için kullanılır.
RBX & EBX: B harfi, base anlamına gelir ve genel amaçlı işlemler için kullanılır ofset adreslerini tutar.
RDX & EDX: D harfi, data anlamına gelir ve genelde çarp ve bölme işlemleri için kullanılır.
RCX & ECX: C harfi count anlamına gelir ve döngü işlemleri için kullanılır.
ESP: Extended stack pointer anlamına gelir ve yığının tepe noktasının adresini işaret eder.
EBP: Extended base pointer anlamına gelir ve RAM üzerinde Saved EBP kısmının adresini işaret eder. Bu kısma Saved EBP ile buffer kısmının ayrıldığı yer de diyebiliriz. Asla RBX ve EBX ile karıştırılmamalıdır, alakaları yoktur.
RSI: Register source index anlamına gelir ve kaynak veri adresini tutmak için kullanılır. Genellikle string işlemleri için kullanılır.
RDI: Register Destination Index anlamına gelir ve hedef adresini tutar. Genellikle string işlemleri için kullanılır.
NOT: Bu işlemleri gözünüzde büyütmenize gerek yoktur aslında “x” ile bitenleri normal bir programlama dilindeki değişkenlere benzetebiliriz. Burada bizim için en önemli olanlar “ESP” ve “EBP” dir.
KOMUT İŞARETÇİSİ (Instruction Pointer)
ESP ve EBP gibi Insturction Pointer’ da bizim üçüncü en önemli başlığımız diyebiliriz. Bu isimlerin hepsi birer pointer olduğundan dolayı aslında çok önemliler.
EIP(Extended Instruction Pointer): Bir sonraki çalıştırılacacak olan komutun adresini işaret eder. Aslında belleğin Buffer kısmında ki Return adresi işaret eder diyebiliriz.
STATUS FLAGS(Durum bayrakları)
ZERO FLAG: Son yapılan aritmetik işlemin sonucunun 0 olduğunu belirtir, eğer sonuç 0 ise flag 1 olur.
CARRY FLAG: Son yapılan işlemde çıkan değer tutulacak olan registerdan büyük ise carry flag 1 olur.
SIGN FLAG: Son yapılan işlemin sonucunun negatif olduğunu gösterir, negatifse bayrak 1 olur.
TRAP FLAG: Debugging için kullanılır eğer bayrak 1 olarak ayarlanmış ise debug moduna geçilir ve kodlar tek tek yürütülür.
SEGMENTS
Segmentler kendi isimlerine göre bellek üzerinde bulunan yerleri işaret ederler.
1. CS(Code Segment) veya Text Segment
2. DS(Data Segment)
3. SS(Stack Segment)
4. HS(Heap Segment)
Registerları anladığımıza göre RAM’e geçebiliriz.
RAM(Random Access Memory)
Ram, geçici bir bellektir işlemci üzerinde bulunan Register kısımları RAM’den daha hızlıdır fakat depolama alanları çok küçük olduğundan dolayı her şeyi depolayamazlar.
Bir program çalıştığı zaman ona RAM’in tamamı verilmez RAM üzerinde küçük bir kısım sanal olarak ona tahsis edilir.
RAM’in yapısı;
Yukarıda gördüğümüz gibi ram üzerinde Code, Heap , Stack , Kernel gibi bölümler bulunuyor. Bunlar RAM’in kısımlarıdır ve işlemciler kısmında konuştuğumuz Segmentler buraları işaret eder. Code kısmını bazen text olarak da görebilirsiniz.
Yukarıdaki resimde bizi ilgilendiren kısım Stack kısmıdır fakat şunu bilmekte fayda var , Heap kısmı veri aldıkça yukarı doğru büyür, Stack ise veri aldıkça aşağı doğru büyür yani ESP’ nin gösterdiği adres küçüldükçe stack büyür.
Yukarıda verdiğim resimde RAM’in en küçük adresi olan 0x00000000 başlangıç adresi ve en büyük adresi olan 0xFFFFFFFF bitiş adresidir. RAM üzerindeki her bir bölümün bir adresi vardır. Bu adresler Hexadecimal olarak gösterilir. Daha da içeri inersek RAM üzerinde çalışacak her Assembly kodunun bile bir adresi vardır. Bu doğrultuda adım adım ilerleyelim.
Şimdi bizi asıl ilgilendiren Stack kısmına giriş yapalım.
STACK
Stack RAM üzerinde bulunan bir bölümdür ve genellikle işlemler burada yürütülür. Aşağıda vereceğim resim RAM’in içerisinde bulunan STACK kısmının yapısıdır. Anlatmak istediğim RAM’in içinde bulunan STACK kısmının yapısını inceliyoruz, yukarıdaki resim ile karıştırmayın.
Yukarıdaki resimde de görüldüğü gibi STACK kısmı da kendi içerisinde kısımlara ayrılıyor. STACK adres küçüldükçe büyüyen bir yapıya sahiptir ve STACK’in en alt kısmı tepesi olarak sayılır. ESP olarak öğrendiğimiz pointer bu kısmı işaret eder.
Orta kısımda bulunan Saved EBP ise, EBP denen pointer’ın işaret ettiği kısımdır. EBP’nin altında kalan kısım buffer kısmıdır. Buffer kısmında verilen değişkenler bulunurken, STACK yapısının üst kısmında, Saved EBP, Return adres ve Argümanlar gibi şeyler bulunur.
NOT: ESP, Buffer kısmının en aşağısını işaret eder ve bizim girdiğimiz değişkenleri tutar.
Return Adres ise bir işlem bittikten sonra dönülecek olan adrestir. Yukarıda da anlattığım gibi burayı EIP denen pointer işaret eder. Burası aslında bir sonraki işlemin adresini tutar.
Daha net anlaşılması açısından sizin için STACK kısmının içeriğini, RAM üzerinde gösteren bir resim daha hazırladım.
Artık gerekli terimleri, RAM ve STACK yapısını öğrendiğimize göre Buffer Overflow saldırısının nasıl gerçekleştiğine bakalım.
STACK üzerinde bulunan Buffer kısmının bizim programa verdiğimiz değişkenleri tuttuğunu biliyoruz. Aslında yukarıdaki resimde local variable denen kısımlar bizim verdiğimiz değişkenleri ifade ediyor. Eğer bu değişkenler istenenden fazla veri alırsa yukarı doğru taşma yaparlar yani buffer kısmından taşıp saved EBP ye gelirler daha sonra yeterince byte verildikten sonra Return adrese taşarlar. Bunu bir sürahinin taşması gibi düşünebiliriz, ne kadar su koyarsam o kadar yukarı çıkar.
Peki bu saldırının amacı ne ?
Yeterince byte olarak veri verdim ve Return adrese kadar taşırma işlemini gerçekleştirdim. Biz biliyoruz ki Return adres bir sonraki çalıştırılacak olan komutun adresini tutar ve EIP gelip bu adresi alarak o komutu çalıştırır. Böylece istediğimiz bir adrese yönlendirme yaparak istediğimiz zararlı kodu çalıştırabiliriz.
Bu karmaşık geldiyse endişelenmeyin detaylıca anlatacağım.
BUFFER OVERFLOW
Bellek taşmasına sebep olan şey C dilinde bulunan strncmp veya gets gibi input alma veya kopyalama fonksiyonlarına benzer fonksiyonlardır. Bu hazır fonksiyonlar kullanıcıdan aldıkları verilerin uzunluklarını kontrol etmeden alır veya kopyalar. Aşağıda bunun için örnek bir C kodu yazdım onun üzerinden bakalım.
Yukarıda görüldüğü gibi, kullanıcıdan bir değer girmesini istedim ve bu değeri gets komutu, oluşturduğum 50 karakter alabilen bir dizinin içerisine atacağım. Burada ki zaafiyet “gets” fonksiyonunun aldığı verinin boyutunu kontrol etmeden kopyalamasıdır, eğer ben 60 karakterlik yani 60 byte’lık bir veri girmiş olsaydım, bu 50 byte alabilen bir dizinin içerisine yazılacaktı ve Buffer kısmı bu dizi için 50 byte değerinde bir alan ayırdığı için 10 byte’lık kısım yukarıya taşacaktır.
Bu taşma değerlerini ve adresleri bize gösteren bazı uygulamalar bulunur. Bu uygulamalara Debugger uygulamaları denir ve bize RAM üzerinde EIP, ESP, EBP gibi pointerların o anda hangi değerleri gösterdiklerini söyler.
ÖNEMLİ NOT: Bunları tek tek, değer gönderek test edeceğiz. Unutmayın Local variable yani buffer kısmından taşmak demek Return adrese gelmek demek değildir, önce Saved EBP ye geleriz ve buradan da taşmamız gerekir. STACK üzerinde her zaman başka veriler bulunabilir. Bu yüzden bazı debug uygulamalarını kullanarak tek tek test etmeli ve ne kadar veri gönderdiğimizde nereye kadar taştığımızı gözlemlemeliyiz. Böylece RAM de bulunan bölümlerin boyutlarını ve EIP, yani Return adrese yazmak için kaç byte değerinde veri göndereceğimizi öğrenebiliriz.
ÖRNEK SENARYO:
Yukarıda bulunan resmi inceleyelim. Resimde kendim bir senaryo oluşturdum bu senaryoda buffer kısmımız 50 byte , saved EBP kısmı 30 byte ve Return adres kısmımız 15 byte değerinde veri alıyor.
- C ile yazdığım zafiyetli uygulamayı başlattım ve bizden bir girdi istedi.
- Girdiye 80 tane “A” yazdım ve sonuna ise /x00/x54/x33/x65 yazdım. Yani toplamda 50 byte’lık alana 84 byte’lık veri yazmış oldum.
- 84 byte yazdığım için 50 byte’lık alan (buffer) doldu ve üst tarafa, Saved ebp ye taştı.
- 84 byte gönderdiğim için 34 byte daha yerleştirmem gerekiyor. Saved EBP’ de kalan 30 byte’ı yerleştirdim ve kalan 4 byte’a istediğim adresi yani /x65/x33/x54/x00 adresini yazdım çünkü 30 byte olan Saved EBP alanı da taştı ve böylece RETURN Address kısmına yazmaya başladım. EIP artık RETURN adrese geldiğinde benim verdiğim adresi görecek ve bir sonraki çalıştırılacak kodun adresi EIP’nin tuttuğu yani Return adresten okuduğu adres olacaktır.
NOT: Her harf bir byte olduğu için “A” yazarak bu alanları doldurdum ve son kısmımda EBP ye ulaştığım bölümde 4 byte olarak çalıştırmak istediğim kodun adresini yazdım.
Uygulama girdi ekranına gireceğim değer şuna benzer oldu;
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/x00/x54/x33/x65
NOT: Yukarıda bulunan 4. madde de /x00/x54/x33/x65 adresini ters yazdığımı düşünebilirsiniz fakat asıl düz olan yani istediğim kodun ram üzerinde çalıştığı adres o dur. Bizim bunu input olarak /x00/x54/x33/x65 şeklinde vermemizin sebebi RAM yapısından kaynaklanır. Büyük Endian ve Küçük Endian olarak iki adet yapısı bulunur.
Dediğim gibi buffer overflow bir debug aracı yardımı ile kaç veri göndereceğini , hangi mimarinin kullanıldığını tek tek göndererek bulmak üzerine çalışır. Eğer ters yazdığınızda kodunuz çalışıyor ise küçük Endian düz yazdığınızda çalışıyorsa Büyük Endian olarak sistem mimarisini tanımlayabiliriz.
Benim yukarıda yaptığım örnekte, 84 byte değerinde bir veri göndermem gerektiğini ve 80 byte dan sonra Return adres üzerine taştığını deneyerek bulmam gerekir.
Peki Return Adrese ne yazacağız ?
Senaryomuz gereği return adrese taştığımız noktanın 80byte dan sonra olduğunu anladık ve 4 byte değerinde bir alan bizim adres yazmamız için yeterli. Fakat biz bu adresi nereden bulacağız ?
Yazacağımız adres bazı uygulamalar yardımıyla bulacağımız, sistemde hali hazırda başka işlemler için çalışan Assembly kodlarının adresleri olabilir. Örneğin başka bir uygulama bir “JMP ESP” komutu çalıştırıyordur ve biz bunun adresini python modülleri yardımı ile bulmuşuzdur, bu adresi taşma kısmımıza yazarsak bu adrese gidilecek ve JMP ESP kodu çalıştırılarak ESP ye yani yığının başına (en altına) atlamış olacağız.
Burada önemli olan şey RETURN ADDRESS çeşitli modüller yardımı ile bulduğumuz komutu yazmaktır.
NOT: Aşağıda verilen adresler örnek olarak verilmiştir ve pratik kısmında somutlaştırılacaktır.
Aşağıdaki resimde görüldüğü gibi eğer RETURN ADDRESS kısmına bir adres yazarsam EIP burada bulunan komut ne ise gidip onu çalıştıracaktır.
Peki şimdi ne değişti ?
Yukarıda yaptığımız işlem şudur;
Artık Return adres de benim eklediğim bir adres bulunuyor. Bu sayede EIP gelip bu adresteki kodu çalıştıracak. Aşağıdaki örnekte /x79/x79/x79/x79 hexadecimal değeri bulunan bir kod çalıştırılmış oldu. Bu gerçek bir kod değildir sadece örnek olması açısından verilmiştir.
Şimdi amacımız bu RETURN ADDRESS kısmına JMP ESP barındıran bir adresi yazarak, Stack kısmının başına atlamak.
UYGULAMA
Giriş
Şimdi bazı debugger uygulamalarını kullanarak bir pratik yapalım.
Eğer linux üzerinde zafiyetli çalıştırılabilir dosya üzerinde denemeler yapacaksanız gdb adı verilen bir debug toolu kullanabilirsiniz. Eğer bu tool ile ilk defa çalışacaksanız size biraz karmaşık ve zor gelebilir, bu yüzden windows üzerinde incelemelerimizi, görsel arayüzlü bir debug toolu olan immunity debugger ile yapacağız.
NOT: Linux üzerinde komut arayüzlü gdb yaygın olarak kullanılsa da, Linux için, immunity debuger ile neredeyse aynı arayüz ve çalışma mantığına sahip Ollydbg isimli toolu da kullanabilirsiniz.
İşlemlerimizi, Github üzerinden indirip deneme yapabileceğiniz bir zafiyetli sunucu üzerinden ilerleteceğiz. İnceleme aşamasını daha anlaşılır olması açısından windows üzerinden yapacağım.
Kullanacağımız sunucunun adı vulnserver.exe. Kendi makinemde çalıştırarak buffer overflow incelemelerini anlatacağım. Amacım size olayın mantığını kavratmak çünkü her bir makinada farklı komutlar ve zafiyetler farklı yöntemler kullanarak ortaya çıkabilir. Sizde kendiniz denemeler yapmak istiyorsanız aşağıya sunucu linkini bırakacağım.
Bu makinanın aynı zamanda C kodu da yanında bulunduğundan dolayı açıp inceleme yapabilirsiniz. Github kısmında da yazdığı gibi sunucuyu çalıştırdığınız zaman sizin 9999 portunuzda çalışacaktır.
Sunucuyu çalıştırdığım windows üzerinde netstat -a komutunu kullanırsam açık portlarımı görebilirim ve bakın 9999 kullanılıyor.
Şimdi Immunity debugger uygulamamızı açıp vulnserver.exe yi attach edip incelemelere başlayalım.
Attach butonuna tıkladıktan sonra önümüze dört parçaya bölünmüş karmaşık sayılar ve harfler bulunan bir ekran gelecektir. Endişelenmeyin şimdi bunların neler olduğuna bakacağız.
Sol üst bölme programın kod kısmını ifade eder bu RAM üzerinde yukarda görmüş olduğumuz CODE/TEXT kısmıdır aslında.
Sol alt bölüm programın hexadecimal ve ASCII olarak içeriğini gösterir.
Sağ alt kısım, verdiğimiz değerlerin yazıldığı RAM adreslerini görmemizi sağlar. Aslında tüm adresleri buradan takip edeceğiz.
Sağ üst kısım ise en baştan beri üzerinde durduğumuz EBP, ESP, EIP ve diğer registerların içeriğini gösterir. Unutmayın amacımız EIP nin işaret ettiği değeri kendi verdiğimiz adres olarak bu ekranda görmek.
Şimdi kendi kali linux makinamdan bu windowsun 9999. portuna bağlanacağım ve bu port üzerinde çalışan sunucuda neler varmış bir bakacağım ama bunu yapmadan önce Immunity debugger dan play işaretine basmayı unutmayın yoksa aynı bir burpsuite proxy gibi isteği keser ve sunucuya bağlanamazsınız.
Gördüğünüz gibi sunucunun içerisinde bir çok komut var. Bunların hepsinde buffer overflow olma ihtimali var. Fakat unutmayın bizim elimizde bir C kodu vardı ve biliyoruz ki strncpy , gets gibi fonksiyonlar zafiyetli fonksiyonlardı. C kodunun içinde bunları arayıp hangi komutta kullanıldıklarını bulabilirim veya tüm parametre alan komutları tek tek taşırmayı deneyebilirim.
Buradaki zararlı olan komut TRUN’ dır ve bunun üzerinden devam edeceğiz.
“TRUN AAAAAA” diyerek denemeler yapıyorum fakat anlaşılan çok yüksek byte değerlerine sahip bu yüzden basit bir python kodu yazarak otomatik payload üretiyorum.
bu kod komut satırı üzerinden python çalıştırarak payload adında bir değikenin içerisine 3000 tane A yazmamı ve daha sonra bunu print ile ekrana yazdırmamı sağlıyor. Şimdi bunu kopyalayıp TRUN’ da deneyeceğim.
Bunu gönderirken sistemin algılaması için şu şekilde göndermem gerekiyor;
TRUN /.:/ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Evet 3000 byte lık A gönderdiğimde hem stack hem saved EBP den taştım aynı zamanda EIP ye bile A harflerini yazdım.
Fakat tam olarak nerden taştığımızı bilmemiz gerek böylece shell kodumuzu ve Return adres kısmına yazacağımız adresimizi doğru bir şekilde STACK’e yerleştirebilelim. Bu işlem için 3000 byte dan küçültürek tek tek denemeler yapabilirsiniz.
Yukarıda gördüğünüz resimde EIP’nin işaret ettiği adres 41414141 olarak gözüküyor. Peki neden A yazmıyor ? 41, A harfinin ASCII karşılığıdır bu yüzden Return adrese A yazdığımız zaman EIP de o 41414141 olarak gözükür yani 4 adet A yazılmış olarak gözükür.
Sağ alt tarafa bakarsak eğer, mavi ile işaret edilen ilk satır ESP yi işaret ediyor, işte burası yığının başıdır. Biliyoruz ki, ESP yığının en tepesinin adresini işaret eder ve sağ yukardan ESP’nin yanında yazan adresten bunu kontrol edebiliriz.
Bu işlemler için python ile exploit geliştireceğiz.
PYTHON İLE EXPLOIT GELİŞTİRME
EIP TAŞMA SINIRININ BULUNMASI
Yukarıda, ilk başta 3000 adet A gönderdiğimiz zaman fazla olduğunu gördük. Bu yüzden tek tek hangi noktada EIP’ye yazmaya başladığımızı buluyoruz. Bunun için hazır scriptler kullanabilirsiniz, fakat tek tek de deneyebilirsiniz. Bu örnekte 2004 adet A harfi gönderdiğim zaman EIP değerinde 1 Adet 41 değeri görüyorum, bu demek oluyor ki 2003 adet A harfi taşmam için yeterli.
Şimdi sıra mona kullanmaya geldi.
MONA İLE JMP ESP ARAMA
Mona python dosyamızı internetten indirip Debugger içerisinde gösterilen konuma eklememiz gerekiyor.
Mona bellek araması yapmamızı sağlayan bir araçtır. EIP’den sonra yazdığımız JMP ESP komutu yardımıyla stack kısmına yazılmaya devam edeceğini bildiğimiz için bu modül kullanılarak JMP ESP assembly komutu aranır ve EIP’ye, buffer içindeki bir nokta değil bu JMP ESP’nin bulunduğu adres yazılarak EIP de bu komut çalıştırılır.
Şu şekilde görünür;
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCXXXXXXXXXXXXXXXXXXXXXXXXXX
Burada A’lar Buffer kısmını B’ler saved EBP’yi ve C’ler ise EIP temsil ediyor
NOT: EIP’den sonra yazacağınız her şeyin stack kısmına yazılacağını unutmayın. Bu yüzden mona modules ile çalışırken JMP ESP komutu SafeSEH ve ASLR’si kapalı modüller üzerinde aranır ve EIP’ye JMP ESP komutunun modül içinde bulunduğu adres verilerek ESP ye atlanmaya çalışılır. SafeSEH ve ASLR ram üzerindeki bu işlemleri zorlaştırmak için kullanılan güvenlik yöntemleridir. Bu yöntemler RAM’de sizi rastgele yerlere atar bu yüzden kapalı olan Modüller kullanılmalıdır. Seçtiğimiz modulün içerisinde ASCII olarak JMP ESP komutu aranmalıdır.
SIRASIYLA;
!mona modules
Bu komut modülleri arar ve getirir. Buradan ASLR ve SafeSEH’i kapalı olan bir modül adı seçilir.
!mona find -s ‘\xff\xe4’ -m <bulduğumuz-modül ismi>
\xff\xe4, JMP ESP’nin Hexadecimal halidir ve bu komut, yukarıda seçtiğimiz modül içerisinde JMP ESP assembly kodunun adresini arar. Bulunan adres EIP’ye verilir.
BAD CHAR TESPİTİ
Kötü karakterleri tespit etmek için bütün hexadecimal değerleri yazdığımız bu python scripti ile göndereceğiz. Bu script karşı sunucuya bağlanıp, oraya aynı telnet ile yaptığımız gibi veri göndermemizi sağlıyor.
Gönderdikten sonra incelemeye başlıyoruz. Bu işlem için sağ üstte bulunan ESP değerini kopyalayıp sol alt köşede aratıp Stack kısmının başına gidiyoruz. İnceleyeme başladığımız zaman tüm karakterlerin aşağıda olduğu gibi yazıldığını herhangi bir bad char değeri bulunmadığını görüyoruz.
NOT: 0x01 olarak listemiz başlıyor, bunun nedeni 0x00 her zaman kötü bir karakterdir test etmemize gerek yok.
Artık bize reverse shell verecek olan shell kodumuzu oluşturabiliriz. Unutmayalım 0x00 bad char, bu yüzden onu exclude edeceğiz.
Kodumuzun son hali aşağıdaki gibi olacaktır. Burada görüldüğü gibi shell kodu eklemeden önce 32 adet 0x90 ekledim. Bu bir NoP kızağıdır ve arada bir boşluk bırakmamıza yarayan “işlem yapma” anlamına gelen değerleri ekler.
Msfconsole üzerinden dinleme işlemini gerçekleştirdikten sonra python kodumuzu çalıştırıyoruz ve meterpreter session geliyor.