Exploit Education: Phoenix ile Linux Stack Tabanlı Zafiyetlerin İstismarı #1

Selamlar, öncelikle başlıktan da anlayabileceğiniz gibi bu yazıda Exploit.Education tarafından hazırlanan Phoenix makinesindeki 3 adet zorluğu çözerek Linux sistemlerde stack buffer overflow yani Türkçesiyle “arabellek taşması” zafiyetlerini temel düzeyde anlamaya çalışacağız. Stack özelinde geriye kalan zorluklara ise inşallah ikinci yazıda detaylıca değinmeyi düşünüyorum. Yazıyı okurken temel işletim sistemi, programlama ve x86 Assembly bilgisi dışında herhangi bir binary exploitation bilgisine sahip olmak zorunda değilsiniz. Sıfırdan herkese hitap edebilecek şekilde yazmaya çalışacağım. Uykusuz geçen birkaç gece boyunca yazdığım için hatalı olduğum noktalar varsa veya şöyle olsa daha güzel olurdu diye uyarmaktan lütfen çekinmeyin.

Ana başlıklarımız:

  • Phoenix Makinesinin Kurulumu ve Çalıştırılması Aşamaları
  • Stack Zero: Buffer Overflow’u Anlamak
  • Ara Başlığımız: Peki neden böyle oldu? Stack nedir ve neden taşar?
  • Stack One: ASCII Tablosu, Bellekte Veri Organizasyonu, Endianness Kavramı
  • Stack Two: Ortam Değişkenleri, Debugging’e Kıyısından Giriş

Mutlaka bilenler vardır, Phoenix’i daha önceleri internette çözümlerini bulabileceğiniz Protostar makinesinin yenilenmiş versiyonu olarak düşünebilirsiniz.  Andrew Griffiths adında bir abimiz tarafından hazırlanan bu makineleri kullanarak ilerleyen zamanlarda format string, heap overflow, network zafiyetleri, Linux temelleri gibi diğer konuları da anlatmayı planlıyorum.

Neyse çok uzatmadan başlayalım.

Phoenix Makinesinin Kurulumu ve Çalıştırılması Aşamaları

İhtiyacımız olanlar herhangi bir Linux kurulu sistem (sanal makine de olabilir), QEMU ve uygulamaları yapacağımız Phoenix makinesinin kendisi. “Debian” paketi olarak da doğrudan indirip kurmanız mümkün lakin zafiyetli programları sisteminizde kurulu tutmanın ne kadar mantıklı olacağı tartışılacağından, biz gerekli “qcow2” imajlarını edinerek QEMU üzerinden başlatacağız.

https://exploit.education/downloads/ bu sayfa üzerinden “AMD64” yazan “QEMU” paketini indirelim. Eğer siz ARM mimarisinde bir işlemciye sahip bilgisayar kullanıyorsanız ona uygun paketi edinmeniz gerekiyor.

phoenix download, indirme

Gerekli paketi indirdikten sonra tar -xvf exploit-education-phoenix-amd64-v1.0.0-alpha-3.tar.xz komutunu kullanarak dizine çıkartalım.

Ben halihazırda Debian kullanıyorum, QEMU kurmak için kendi sitesinde yazdığı gibi doğrudan sudo apt-get install qemu yazmak ne yazık ki yeterli gelmiyor. Emülatörün çalışması için komple paket olarak edinilmesi lazım.

Bu nedenle sudo apt install qemu qemu-kvm diyerek KVM ile beraber QEMU’yu sistemimize yüklüyoruz.

qemu, qemu kurulumu

Artık makinemizi QEMU ile başlatabiliriz, bunu kolayca yapabilmemiz için arşivin içerisinde boot-exploit-education-phoenix-amd64.sh adında bir bash scripti yer alıyor. Ben hiçbir değişiklik yapmadan makineyi çalıştırmak istediğim için buraya dokunmadım. Fakat siz burayı düzenleyerek Phoenix’in sahip olduğu bellek miktarını arttırabilir, emülatörün hangi portlardan bağlantı kuracağını belirleyebilirsiniz.

exploit education phoenix kurulumu, boot

chmod +x boot-exploit-education-phoenix-amd64.sh ile scripti yürütülebilir hale getiriyor ve hemen sonrasında çalıştırıyoruz. Artık zorlukları çözmeye başlayabiliriz.

Giriş bilgileri root:root veya user:user şeklinde. Hangi kullanıcıyla oturum açtığınız şu anlık çözdüğümüz challenge’lar için herhangi bir önem arz etmiyor.

qemu ile imaj başlatma

Eğer QEMU üzerinden doğrudan makineye login olduğunuzda Türkçe klavyenin desteklenmediğini göreceksiniz, bu nedenle ben SSH ile makineye bağlanıp kendi terminalim üzerinden zorlukları çözmeyi tercih ediyorum. Size de bu şekilde yapmanızı tavsiye ederim. Makine 2222 portundan 127.0.0.1 localhostta çalıştığı için ssh -p 2222 user@localhost ile bağlantı kuruyorum.

ssh ile phoenix makinesine bağlantı

Zorluklar /opt/phoenix altında yer alıyor. Bu yazıda Intel x86 mimarisindeki programlarda yer alan stack tabanlı zafiyetlere göz atacağımızdan bizim zorluklarımız /opt/phoenix/i486 altında bulunuyor.

Stack Zero: Buffer Overflow’u Anlamak

Bunlardan birincisi “Stack Zero”. Amacı bize buffer yani arabellek nedir, arabellek nasıl taşar, programın akışı bu şekilde nasıl manipüle edilebilir basitçe göstermek. İstediği şey ise changeme olarak belirlenmiş değişkenin üzerine yazarak bu değişkenin işaret ettiği verinin değiştirilmesi.

Ek olarak yardımcı olması açısından bize programın kaynak kodlarını da vermiş durumda. Kaynak kodu okuduğumuzda buffer adı verilen değişkene char buffer[64] kullanılarak “0 ile 63 arasında, kendileri dahil olmak üzere” 64 karakterin, dolayısıyla 64 baytın sığacağı bir arabellek atanıyor. Yani bilgisayarınızın belleğinde bu kadarlık yer ayrılıyor.

Program çalıştıktan sonra dışarıdan verdiğimiz karakter dizileri bu arabelleğe alındıktan sonra changeme adı verilen bir başka değişkene 0 değeri atanıyor. Daha sonraki If, Else blokları ise bu değişkene atanan değeri başarıyla değiştirip değiştiremediğimizin kontrolünü sağlıyor.

phoenix, stack zero

O halde elimizdeki kullanarak şöyle bir yol izliyoruz.

Bizden alınan karakter sayısı kodlardan da anlayabileceğimiz üzere herhangi bir kontrole tabi tutulmuyor. Haliyle 64 bayttan daha büyük bir karakter dizisini programa verirsem ayrılan arabellekten taşan karakterler diğer değişkenin işaret ettiği değerin üzerine yazılacak.

Haydi deneyelim.

64 tane karakteri tek tek klavyeyle oluşturmaya üşendiğim için bu iş için Python kullanmayı seviyorum. Exploitation ile uğraşacaksanız Python 2 veya 3, versiyon fark etmeden herhangi birini temel seviyede bilmenizde yarar var. Ben alışkın olduğum için Python 2 kullanacağım.

İlk başta 63 tane karakter oluşturup programa verelim, bakalım amacımıza ulaşabilecek miyiz?

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-zero 
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Uh oh, 'changeme' has not yet been changed. Would you like to try again?

Gördüğünüz gibi değişkenin değerini manipüle edemedik. Bunun nedeni halen ayrılan arabellekte 1 karakterlik yer daha olması. O halde 1 tane daha A ekleyip 64 karakter gönderelim.

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-zero 
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Uh oh, 'changeme' has not yet been changed. Would you like to try again?

Yine istediğimizi elde edemedik çünkü sınırdayız. Artık arabellek doldu. Bunun üzerine yazacağımız herhangi bir karakter arabelleği taşırıp changeme için ayrılan yere yazılacak. 65 tane A gönerdiğimizde bunu görebiliriz.

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-zero 
Welcome to phoenix/stack-zero, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Well done, the 'changeme' variable has been changed!

Ara Başlığımız: Peki neden böyle oldu? Stack nedir ve neden taşar?

Stack dediğimiz şey, bilgisayarlarımızda programların değişkenlerin işaret ettiği verileri saklamak için kullandığı bir bellek alanı. Programa verilen bir değer saklanacak ve daha sonra tekrardan kullanılmak üzere dışarı çıkarılacaksa bu değerler program için stack üzerinde tahsis edilen alanda tutulur. Bir de bunun dinamik olanı heap var lakin bu yazıda konumuz o değil. Yani her programın kullanacağı alan, programın ihtiyacı kadar ona özel olarak bellekteki bir yerde tahsis edilir ki programlar çalışırken birbirini etkileyip bozmasın. Stack’i ihtiyacımız oldukça bir şeyleri içine atıp geri çıkarabildiğimiz özel bir kutu veya kova gibi düşünebilirsiniz.

Fakat bunun bir istisnası mevcut, memory leak dediğimiz mevzu. Kötü yazılmış programlarda, söz konusu program kullandığı bellek alanını bırakıp işletim sistemine geri vermediği için sürekli şişer, çalışmayı devam ettirebilmek için ihtiyacı olandan çok daha fazla bellek alanını gasp eder.

heap, memory leak, heap allocate
Dinamik bellek alanı Heap’te olan bir memory leak. Stack’te memory leak olmaz, sadece kötü yazılmış bir programda sonsuz yineleme hatası yapılabilir.

Bir su kabı hayalleyelim ve bu su kabının bir seviyesi olsun. Biz kabın alabileceğinden daha fazla su koyduğumuzda ise seviyeyi aştığımız için hemen yanındaki kaba su dolsun. Benzer şekilde stack de böyle taşar. Stack üzerinde programımız için ayrılan alandan daha fazlasını kullanmaya çalışırsak programdaki başka bir alan istila edilecektir. O alanın da sınırlarını zorlarsak en son işletim sistemi kernel’ı araya girip bizi durduracak ve muhtemelen “segmentation fault” hatası alacağız. Zira su kabı örneğinde olduğu gibi ikinci kabı da taşırırsak ortalığın batması muhtemel 😀

Bir Programda Stack Buffer Overflow Zafiyetinin Yer Alması Ne Anlama Gelir?

Bu zafiyet, programa bağlı olarak genelde çalışma akışını normal olmayan bir şekilde değiştirmeye imkan tanır. Saldırgan, sağladığı girdiler sayesinde taşan arabelleğin üzerine birtakım bilgiler yazarak hedefte kod çalıştırabilir. Bu yüzden buffer overflow tarzı güvenlik açıkları oldukça tehlikelidir ve kullanıcıdan girdi alınan, bellek yönetimli dillerle geliştirilmiş bütün yazılımlarda görmek mümkündür diyebiliriz.

Örneğin internette gezinirken bir kötü amaçlı bir görsele denk geldiniz. Eğer görseli işleyen yazılımda exploitable herhangi bir türden buffer overflow zafiyeti varsa; görselde saklanmış birtakım zararlı kodlar bu sayede sisteminizde yürütülebilir ve sisteminizin kontrolü siz herhangi bir şey yapmadan, daha ne olduğunu anlamadan saldırganın eline geçebilir.

Stack One: ASCII Tablosu, Bellekte Veri Organizasyonu, Endianness Kavramı

Şimdi ikinci zorluğumuza bakabiliriz.

“Stack One” adındaki bu seviyede bize öğretmeyi hedeflediği şey yine “Stack Zero”da olduğu gibi değişkenlerin değerlerinin nasıl değiştirilebileceği ve bu verilerin bellekte nasıl düzenlendiği. Yine bize kaynak kodunu vermiş durumda. İncelediğimizde programın bir farkla neredeyse aynı işi yaptığını görüyoruz. Bu sefer kullanıcı tarafından buffer değişkeni taşırıldığında changeme üzerine yazılan değerin hexadecimal yani onaltılık tabanda 0x496c5962 değerine tekabül edip etmediğini kontrol ediyor. Kullanıcı tarafından girilen değeri ise doğrudan argüman olarak alıp kontrolü sağlıyor. Yani önceki örnekte olduğu gibi kullanıcı programı çalıştırdıktan sonra bir değer girilmesini istemiyor, ./stack-one AAAAA şeklinde bir dizi göndermemiz gerekiyor.

Basitçe birkaç karakterlik A dizisini gönderelim ve çıktıya bakalım.

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-one AAAAA
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Getting closer! changeme is currently 0x00000000, we want 0x496c5962

Burada “Stack Zero”dan farklı olarak değiştirmeyi hedeflediğimiz changeme değişkeninde yer alan veriyi de görebiliyoruz. Bu sayede arabelleği taşırıp taşırmadığımızı, istenilen değeri doğru bir şekilde verip vermediğimizi de anlayabiliyoruz.

Fakat hemen öncesinde bizden arabelleği taşırdıktan sonra hangi değerleri yazmamız isteniyor onu öğrenelim. Bildiğiniz gibi bilgisayarda bellekte yer alan veriler kolay ifade edilebilmesi bakımından hexadecimal yani onaltılık sayı sistemi ile ifade edilirler. Zira tutulan verileri binary ve decimal olarak yazmaya kalksak inanılmaz uzun olur.

Bu sayı sistemindeki bazı değerlerin ASCII tablosu dediğimiz bir çeşit standardizasyonda karşılığı vardır. Örneğin hexadecimal olarak 41 sayısı (aslında 0x41, burada 0x ibaresi değerin 16’lık tabanda yazıldığını ifade eder) A harfine denk gelir. Böylece yazılar yazabilir, çıktılar alabilir ve okuyabiliriz.

Örneğin; rootkit.com.trdiye bir karakter dizisi bu anlattıklarımıza göre (tırnaklar olmadan) onaltılık tabanda

72 6F 6F 74 6B 69 74 2E 63 6F 6D 2E 74 72

şeklinde yazılır.

ascii tables, ascii tablosu, referans

Bu bilgiyi unutmadan tekrardan çözmeye çalıştığımız zorluğa dönelim ve 65 karakter göndererek arabelleği taşıralım.

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-one `python -c 'print "A"*65'`
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Getting closer! changeme is currently 0x00000041, we want 0x496c5962

65 tane A yani hexadecimal 41 gönderdiğimizde değişkenimizin 0x00000041 olduğunu görüyoruz. İsterseniz 64 karakterden sonra ABCD şeklinde bir karakter dizisi göndererek bu 4 baytlık boşluğu dolduralım ve değişkenimiz ne hale geliyor görelim. (Bir bayt iki bitten oluşur, 32-bit mimarisinde olduğumuz için 8 bit = 4 bayta denk gelir. Mimariye 32-bit dememizin nedeni de aslında budur, 8×4=32.)

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-one `python -c 'print "A"*64+"ABCD"'`
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Getting closer! changeme is currently 0x44434241, we want 0x496c5962

Yukarıdaki çıktıya baktığınızda bir gariplik dikkatinizi çekmiş olmalı. 64 tane “A” göndererek arabelleği taşırdık. Sonrasında changeme değişkeninin 4 baytlık boşluğuna “ABCD” yazdık ama sayılar sanki ters mi ne? Normalde ABCD gönderdiğimizde basitçe bunların hexadecimal karşılıkları olan 41424344 ile karşılaşmamız gerekmez miydi?

Kafanız çok fazla karışmış olabilir, bu durum bilgisayar mimarisinde “endianness” olarak ifade edilir. Özet ve en sade haliyle açıklayacak olursak bizim için önem arz eden bilginin yer aldığı byte soldan sağa ikili bit şeklinde en başta yazılıyorsa “big-endian”, sağdan sola yine ikili bitler ile yazılıyorsa “little-endian” yapısı kullanılıyor diyoruz. Günümüzde birçoğumuz Intel mimarili (işlemci markası olarak değil, teknoloji olarak) işlemciler kullandığımız için Intel en önemli byte’ı Arapça’da olduğu gibi sağdan sola kabul etmiş. Eski nesil PowerPC gibi işlemcilerde ise “big-endian” kullanıldığından en önemli bayt en solda yer alıyordu.

Bu nedenle “stack-one”a gönderdiğimiz “ABCD” yani normalde 41424344 olan verimiz 44434241 şeklinde bellekte yer edindi. Eğer “big-endian” bir işlemci kullanıyor olsaydık ekranda doğrudan 41424344 görüyor olurduk.

Konu biraz geniş olduğu için sizin okumanıza bırakıyorum. İlgili Wikipedia sayfasında detaylarını bulabilirsiniz. Bizim için şu anlık anlattıklarımız yeterli.

0x496c5962 değerinin kaç olduğuna gelecek olursak, hemen basitçe bir Hex converter sitesine girdiğimizde “IlYb” karakterlerine denk geldiğini görebiliriz fakat bunu doğrudan bu şekilde verdiğimizde yukarıda anlattığımız mevzudan dolayı istenen verinin tersini yazmış oluruz.

hex to text, onaltılık tabandan metine dönüştürme
user@phoenix-amd64:/opt/phoenix/i486$ ./stack-one `python -c 'print "A"*64+"IlYb"'`
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Getting closer! changeme is currently 0x62596c49, we want 0x496c5962

0x62596c49 yani “bYlI” olarak değeri gönderdiğimizde başarıya ulaştığımızı görüyoruz.

user@phoenix-amd64:/opt/phoenix/i486$ ./stack-one `python -c 'print "A"*64+"bYlI"'`
Welcome to phoenix/stack-one, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value

Tabi bu şekilde alışmanız doğru değil zira gerçek programlar üzerinde exploitationla uğraşırken ASCII karşılığı biz insanlar için anlamsız olan veya terminalde gözükmeyen bazı karakterleri de vermeniz gerekebilir. Bu nedenle bundan sonraki zorluğu çözerken 64 bayt A yazdıktan sonra bizden istenen değeri hexadecimal olarak yazacağız.

Stack Two: Ortam Değişkenleri, Debugging’e Kıyısından Giriş

Hayat takdir edersiniz ki her zaman (aşağıda kaynak kodunu bize vermesi gibi) bu kadar kolaylıklar sunmuyor.

Diyelim elimizde herhangi bir kaynak kodu olmasaydı, buffer değişkenine ait arabelleğin kaçıncı nasıl taşacağını ve zorluğun bizden hangi değeri nereye girmemizi istediğini nereden bilebilirdik?

Tersine mühendislik kavramına hoş geldiniz. “Stack Two” ile “Stack One” tek bir fark dışında (environment variables kontrolü) neredeyse birebir aynı olduğundan ne yaptığımızı anlamlandırmak için nihayet biraz elimizi kirletip “debugging” yapacağız. Tersine mühendisliğin kavramsal olarak ne olduğuna yabancı olanlar varsa PwnLab.Me ekibinden ayrılmadan önce yayınlamış olduğum Tersine Mühendisliğe Giriş – Temel Kavramlar isimli yazıyı okuyabilirler.

Linux sistemlerde bu iş için gdb, nam-ı diğer GNU Debugger’ı kullanacağız. Zaten debugging söz konusu olduğunda alternatifi yok gibi bir şey diyebiliriz. Bu nedenle gdb kullanımını öğrenmeniz Linux tarafında binary exploitation ile ilgileniyorsanız oldukça önemli.

Uygulamayı gdb ile başlatmak için gdb stack-two şeklinde bir komut kullanıyoruz. C ile yazılmış programlarda genellikle asıl mevzunun döndüğü yer, main fonksiyonudur. Bu nedenle disassembly main veya kısa haliyle disas main yazarak bu kısmı disassemble edip inceliyoruz. GNU Debugger varsayılan olarak disassembly ettiği kodları AT&T syntax ile sunar. Ben Intel syntax’e alıştığım için set disassembly-flavor Intel diyerek inceliyorum.

gdb ile disassembly, tersine mühendislik, linux

Burada bazı temel x86 Assembly komutlarını anlayabiliyor olmamız gerekiyor. Programın akışına yukarıdan aşağıda doğru bakacak olursak zorluğu çözmemiz için gereken “ortam değişkeni” bilgisi ile alakalı getenv() fonksiyonu 0x0804856e satırında CALL instruction’ı ile gözümüze çarpıyor. Yani bu fonksiyona bir çağrı yapılmış ama hangi girdiyle? Eğer bu girdiyi bulabilirsek o bizim ortam değişkenimizdir diyebiliriz.

x86 Assembly’de stack alanına yani “veri saklama kutumuza” herhangi bir bilgi koyarken veya onu geri alırken PUSH ve POP komutlarını kullanırız. PUSH adı üstünde “ittirmek” yani veriyi yazmak iken, POP yazılan veriyi çıkarmaya yarar. 0x0804856e adresinde getenv()‘e bir çağrı yapıldığına göre buna giden argüman da o zaman bir üstündeki yani 0x08048569‘da yer alan PUSH komutundaki veridir.

Ortam değişkenimizin öğrenelim. gdb üzerinde bir adresteki veriyi hexadecimal string olarak okumak istersek x/s komutunu adres bilgisiyle beraber kullanırız.

(gdb) x/s 0x804867a
0x804867a:	"ExploitEducation"

Değişkenimiz bu. O halde gdb’den çıkıp programa istediğini verelim.

user@phoenix-amd64:/opt/phoenix/i486$ ExploitEducation=`python -c 'print "A"*64+"\x0a\x09\x0a\x0d"'` && ./stack-two
Welcome to phoenix/stack-two, brought to you by https://exploit.education
Well done, you have successfully set changeme to the correct value

Yazıyı eğer sonuna kadar uygulayarak okuduysanız; stack buffer overflow zafiyetleri nasıl ve neden oluşur, ASCII tablosu, hexadecimal ve decimal nedir, endianness kavramı neyi ifade eder gibi konular hakkında artık temel seviyede bilgiye sahipsiniz. Sormak veya eklemek istediğiniz bir şey olursa Twitter’dan, mailden veya yorumlara yazmaktan çekinmeyin.

Ayrıca bu alana meraklıysanız Phrack dergisinde yayınlanmış olan “Smashing The Stack For Fun And Profit” makalesini kesinlikle okumanızı tavsiye ederim. Başlangıç için biraz karışık gelebilir, sonraki yazılarla beraber yavaş yavaş anlamlandıracağız inşallah.

Saygılarımla. – M. Akil Gündoğan (0xr3act0r)

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir