lf183, Software Development: Bir uygulamanın geliştirilmesinde güvenliğin istenmeyen durumlarından kaçışlar - Bölüm 2:hafıza, yığıt ve fonksiyonlar, kabuk kodu
Christophe Blaess bağımsız bir havacılık mühendisi.O bir Linux meraklısı ve birçok işini bu sistem yardımıyla yapıyor.Linux Dökümantasyon Projesi tarafından yayınlanan kişisel sayfaların çevirisinin organizasyonunu yapıyor.
Chritophe Grenier ESIEA'da beş yıldır öğrenci ve aynı zamanda burada sistem yöneticisi olarak çalışıyor.Bilgisayar güvenliğine karşı bir tutkusu var.
Frédéric Raynal birçok senedir Linux kullanıyorlar çünkü o kirletmiyor, hormonları kullanmıyor, ne GMO ne de hayvansal yağ...sadece ter ve oyunlar.
Yığıt ve küme
Kabuk kodu oluşturma
C dili ile
Makine dili çağrıları
Hafızanın içine kabuk kodunun yerleştirilmesi
Null bytes problemi
Kabuk kodunun inşaası
Genelleme ve son ayrıntılar
Bir uygulamanın geliştirilmesinde güvenliğin istenmeyen durumlarından kaçışlar - Bölüm 2:hafıza, yığıt ve fonksiyonlar, kabuk kodu
Çeviri : Coşkun Demirboğa
Bu makale serileri uygulamalarda ortaya çıkan genel güvenlik sorunlarını vurgulamak amacıyla oluşturulmuştur.Bu gibi istenmeyen sorunların çeşitli gelişmeler yardımıyla nasıl aşılacağını göstermektedir.
Bu makale, hafıza organizasyonu/yayılımı, fonksiyon ve hafıza arasındaki ilişki konuları üzerinde yoğunlaşmıştır.Son bölüm kabuk kodu'nun nasıl oluşturulacağı konusunu ele almaktadır.
Bir önceki makalemizde basit güvenlik sorunlarını analiz etmiştik.Bunlardan bir tanesi harici komutların yerine getirilmesi üzerineydi.Bu makale ve bundan sonraki,daha geniş çeşit saldırıları anlatacak.İlk olarak çalışan uygulamalardaki hafıza yapılarına çalışacağız ve kabuğu başlatmak için küçük bir kod yazacağız.(kabuk kodu)
Talimat kümesinden oluşan bir program düşünelim, makina koduyla ifade edilsin ( yazmak için kullanılan dil gözardı edilsin ),işte biz bunlara binary diyoruz.Binary dosyasını derlerken program kaynağı değişkenleri, sabitleri ve talimatları tutar.Bu bölüm binary dosyasının değişik bölümlerini hafıza yayılımını sunuyor.
Binary dosyası çalıştığında neler olduğunu anlamak için, hafıza organizasyonuna bir bakalım.Değişik alanlara itimat etmektedir :
Genellikle ama herzaman değil, biz sadece bu makale için önemli olan bazı bölgelere yoğunlaşacağız.
size -A file --radix 16 komutu derlemek için gerekli her bir alanın boyutunu verir.Burdan hafıza adreslerini alırsınız (objdump komutu bu bilgiye ulaşmak için kullanılır). Bu size komutu binary olarak "fct":
.fini              ro      0x14   0x8048468
text alanı program talimatlarını tutmaktadır.Bu alan sadece-oku.Bu aynı binary dosyasını çalıştıran bütün işlemler arasında paylaşıldı.Bu alana yazmaya teşebbüs etmek segmentasyon ihlali hatasına sebep olur.
Diğer alanları açıklamadan önce gelin C'deki değişkenler ile ilgili birkaç şey söyleyelim.global değişkenler tüm program da kullanılır ancak local değişkenler sadece fonksiyonlarla birlikte kullanılır. static değişkenler bilindik bir boyuta sahiptirler.Boyutları çeşitlerine bağlıdır.Çeşitleri ise örneğin;char, int, double, işaretçiler vs.Bir işaretçibir adres işaret eder bir hafıza ile birlikte.Bu bir PC tipi makinada 32bit 'tir.Bilinmeyen birşey varki o da derleme sırasında işaretçinin hangi alana doğru olduğudur.Bir dynamic değişkeni bir hafıza bölgesi ve bunlara işaret eden işaretçileri (işaretçinin kendisi değil adresi) ifade eder.
Verilen işlem için hafıza organizasyonuna geri dönelim.data alanı global duragan verileri saklar ( bu veri derleme zamanını sağlar ), bss parçası başta yer almayan global verileri tutar.Bu alanlar derleme zamanı için ayrılmıştır ve boyutları tuttukları nesnelere göre tanımlanmıştır.
Yerel ve dinamik değişkenler bir hafıza alanı içinde gruplandırılır. Bu hafıza alanı programın çalışması için ayrılmıştır.(user stack frame). Fonksiyonlar yardıma çağrılır, yerel değişkenler için gelişmiş örnekler bilinmemektedir.Bunları tanımladığımız zaman bunları bir stackiçine koyacağız.Bu yığıt en yüksek adreslerin üzerindedir ve LIFO modeline göre çalışır(Last In, First Out).user frame altındaki alan dinamik değişkenlerin tahsisine kullanılmıştır.Bu alan heap şeklinde tanımlanır ve işaretçiler ve dinamik değişkenler tarafından adreslenen hafıza alanlarını tutar.Açıkça görülüyorki, bir işaretçi 32bit ya BSS veya yığıtta.Tahsis edildiğinde ilk ayrılmış byte'a karşı gelen bir adres alır.
Sıradaki örnek hafızadaki değişken yayılımını örneklemektedir:
gdb debugger tüm bunları onaylayacaktır.
Gelin bu f() fonksiyonuna bir aralık bırakalım ve programı bu noktayakadar çalıştıralım :
Şimdi değişkenlerin yerlerini görebiliyoruz.
Komut 1 (print &index) hafıza adresini index global değişken için gösteriyor.İkinci talimat (info) birleştirme sembolü veriyor bu adres ve hafızadaki yer için:index, global durağan değişkeni bu data alanda saklanmaktadır.
3.ve 4. talimatlar başta gelmeyen durağan değişkenleri nothing, BSS parçası içinde yer almaktadır.
5.satırda str ...komutu görülüyor.Gerçekte str değişken içermektedir, ve bu adres 0x80495a8.Talimat 6 hiçbir değişken bu adreste tanımlanmamıştır.Komut 7 str değişken adresini almaya izin verir ve komut 8 onu işaret eder ve BSS parçasında bulunabilir.
9'da 4 byte adresteki hafızaya karşı gelir 0x804959c : Bu bir ayrılmış adresle birlikte kümeye karşı gelir.10'un içeriğinde şu string vardır "abcde" :
Yerel değişkenler c ve i yığıtın içinde yer alır.
size komutu sayesinde boyutun geri döndüğüne dikkat çekmek istiyoruz.Ancak programa baktığımızda farklı alanlar beklediğimiz gibi uyum sağlamaz.Bunun sebebi program çalıştırıldığında kütüphanede değişik değişkenlerin varolmasıdır.(type info variables under gdb to get them all).
Her seferinde fonksiyon tanımlandığında, yeni bir çevre terel değişlenler için gerekli hafıza ile birlikte ve fonksiyon parametreleri ile birlikte oluşturulur (burda environment fonksiyon çalıştırılırken tüm elementler görüleceği manasına gelir : argümanları, yerel değişkenleri,çalışan yığıtlarında varolan geri dönüş adresleri...fakat bir önceki makalede bahsettiğimiz kabuk değişkenleri için çevre değil). %esp (extended stack pointer) kaydı en yüksek yığıt adreslerini tutar.
Yerel değişkenlerin yığıtlarla birlikte adresleri göreceli karşılık olarak %esp ifade edilebilir.Başlıklar herzaman eklenip çıkarılabilir yığıtlardan ve her değişkenin karşılığı ayarlanabilir.Bu çok etkisizdir. İkinci kayıtın kullanılması şu gelişmeye yol açar : %ebp (genişletilmiş taban işaretçisi) başlangıç adresini tutar.Böylece, kayıtla ilgili offset açıklamak yeterli olur.Fonksiyon çalışırken sabit kalır.Parametreleri bulmak veya fonksiyonları yerel değişkenler ile birlikte kolay değildir.
Yığıtların temel birimleri word : i386 CPU'lar üzerinde 32bit ve 4 byte'dır.Örneğin, Alpha CPU'ları bir kelime için 64bit yer ayırır. Yığıtlar sadece kelimeleri yönetir.Bunun anlamı her yer ayrılmış değişken bazı kelime numarası kullanır.İlerde fonksiyonların açıklamasında ayrıntılı bir şekilde göreceğiz. str gösteriminde değişken gdb kullanılışını içerir.bir önceki örnek bunu örnekliyor. gdb x komutu tüm cümleyi gösteriyor (soldan sağa oku little endian açıklamasına kadar.
Yığıt 2 cpu talimatı tarafından kontrol edilebilir :
push value : azaltır %esp kelime ile birlikte, bir sonraki kelimenin adresini almak için, saklanan value komutun argüman olarak verilmesi gereklidir.Bu talimat yığıtın en üst noktası üzerindeki değeri verir;
pop dest : adreste tutulan değeri verir ve %esp tarafından işaret edilir ve dest'in içinde bulunur ve bu kayıt içeriğini yükseltir.En yüksekteki yığıt başlığını yerini değiştirir.
Kayıtlar gerçekte nedir?Onları yalnızca birer çizici olarak görebilirsiniz, bir kelime serisinden oluşmuşlardır.Her seferinde kayda yeni bir kelime girer ve eskisi kaybolur.Hafıza ve CPU arasında direk bir iletişime izin verir.
İlk olarak 'e' kayıtlarda görünür ve "extended" manasına gelir ve 16bit ile 32bit arasındaki evrimi işaret eder.
Kayıtlar dört sınıfa ayrılırlar :
genel kayıtlar : %eax, %ebx, %ecx ve %edx veriyi yönetmek için kullanılır;
parça kayıtlar : 16bit %cs, %ds, %esx ve %ss, hafızanın ilk parçasını tutar;
karşılık kayıtlar :bunlar parça kayıtlar ile ilgili bir karşılık işaret ederler;
%eip (Extended Instruction Pointer) :bir sonraki talimatın çalışması için gerekli olan adresi işaret eder;
%ebp (Extended Base Pointer) :fonksiyon için yerel çevrenin başlangıcını işaret eder;
%esi (Extended Source Index) :hafıza bloğunu kullanan bir operasyondaki veri kaynağının karşılığını tutar;
%edi (Extended Destination Index) :hafıza bloğunu kullanan bir operasyondaki varılacak veri karşılığını tutar;
%esp (Extended Stack Pointer) :yığıtın en üst noktası;
özel kayıtlar :bunlar sadece CPU tarafından kullanılır.
Burada ayrıntı verilmemiştir, ancak aynı sınıftan olan kayıtlar aynı şeyler zannedilmemelidir.
Bu bölümde bir programın başlangıcından sonuna dek olan davranışı sunulacaktır. Bu bölüm boyunca şu örneği kullanacağız :
Bu bölümün amacı yukarıda yer alan fonksiyonların davranışlarını yığıtlara ve kayıtlara dikkat ederek açıklamaktır.Bazı saldırılar bir programın çalışmasını değiştirebilirler.Bunları anlamak için nelerin düzgün çalıştığını bilmek gereklidir.
Bir fonksiyonun çalışması üç aşamaya ayrılır :
prolog : Bir fonksiyon girildiğinde aynı zamanda siz onun çıkış yolunu hazırlamış oluyorsunuz.Yığıtları kaydetmek için fonksiyonlar girilmeden ve saklanmadan önce hafıza olması gerekir;
call fonksiyonu : Bir fonksiyon çağrıldığında onun parametreleri yığıtın içinde yer alır ve işaretçi talimat kaydedilmiştir, çünkü bu talimatın çalıştırılması fonksiyonun sağından işleme devam edilmesini sağlar;
return fonsiyonu : Bu fonksiyon fonksiyon çağrılmadan önce gerekli şeylerin geri çağrılmasını sağlar.
Bir fonksiyon herzaman talimat ile başlar :
Bu üç talimat prolog'un dedilerini tapıyor.diagram 1 ayrıntıları toto() fonsiyonunun başlangıcının %ebp ve %esp kayıt parçalarının nasıl çalıştığını açıklamaktadır :
Başlangıçta, %ebp noktaları hafızada herhangi bir X adresini işaret eder.%esp yığıtta en düşük yerdedir Y adresinde ve son yığıt giriş noktasını işaret eder.Bir fonksiyon girildiğinde "geçerli çevre" 'yi kaydetmek zorundasınız.Bu %ebp'dur.%ebp'den beri yığıtın içine konur ve %esp hafıza sözcüğü tarafından azaltılır.
Bu ikinci talimat fonksiyon için yeni bir "çevre" inşa edilmesini %ebp kodunu yığıtın en başına konulması ile sağlar.%ebp ve %esp bir önceki çevre adresini tutarlar ve aynı hafıza noktasını işaret ederler.
Şimdi yerel değişkenler için yığıt boşluğu ayrılmış durumdadır.Karakter sırası beş başlıkla tanımlanmıştır ve 5 byte (bir char bir byte) yere ihtiyaç vardır. Bununla birlikte yığıtlar sadece words'ü yönetirler ve sadece word'ten oluşan birkaç (1 word, 2 words, 3 words, ...) yer ayırırlar.4 byte yerine 5 byte word saklamak için 8 byte ( 2 words) kullanmanız gerekli. String'in bir parçası olmasa dahi bu parça kullanılabilir.k tam sayı 4 byte kullanır.Bu boşluk %esp'un 0xc tarafından azaltılması için kullanılır. Yerel değişkenler 8+4=12 byte kullanırlar (örneğin 3 sözcük).
Bu mekanizmadan başka burda hatırlanması gereken önemli bir nokta da yerel değişkenlerin yeridir : yerel değişkenler %ebp ile ilgili olarak negative gibi bir karşılığa sahiptirler. i=0 talimatı main() fonksiyonu içinde bunu örnekler.Makine kodu i'yi çalıştırmak için direkt olmayan bir adresleme yöntemi kullanır :
0xfffffffc heksedesimal -4 tam sayısını ifade eder. Bu notasyonun anlamı 0 değerini "-4 byte" ın bulunduğu %ebp kaydına yerleştir. i ilk ve tek yerel değişkendir main() fonksiyonunun içinde yer alan.Böylece onun adresi 4 byte (örneğin tam sayını boyutu) %ebp kaydının altındadır.
Fonksiyonun başında yapıldığı gibi fonksiyon kendi çevresini hazırlar, fonksiyon bu fonksiyona argumanlarını alması için izin verir ve çağıran fonksiyona geri dönerek işlemi bitirir.
Örnek olarak toto(1, 2); çağrısını ele alalım.
Fonksiyon çağırmadan önce argumanlar yığıtta saklı olmalıdırlar.Bizim bu örneğimizde, iki sabit tam sayı 1 ve 2 birinci yığıttır ve sonuncuyla başlar. %eip kaydı bir sonraki talimatın adresini çalışması için tutar.
call talimatını çalıştırırken %eip bir sonraki talimatınadres değerlerini alır.Tüm talimatlar aynı boşluk için kullanılmaz fakat bu CPU 'ya bağlıdır. call yardımı ile %eip içindeki adres kaydedilebilir.Bu işlemden fonksiyonu çalıştırdıktan sonra geriye dönmek için gereklidir :
Bir argüman gibi değerlendirilip call'e göndermek için bir değer verilmiştir.Bu da talimatın başındaki toto() fonksiyonun adresine karşılık gelir.Bu adres sonra %eip'a kopyalanır.Böylece yeni talimat çalışmaya başlar.
İlk önce biz fonksiyonun içindeyiz. 'nun argümanları ve geri dönen adresin positive karşılığı vardır %ebp ile ilgili olan.Bir sonraki talimat bu kayıtı yığıtın en üstüne yerleştirir.j=0 talimatı toto()'ın içindedir.Fonksiyon bunu örnekler.Makina kodu tekrar bu indirekt adreslemeyi j'yi çalıştırmak için kullanır :
0xc heksedesimali +12 tam sayısını ifade eder. Bu notasyon "+12 byte" da bulunan 0 değerini ifade eder. j fonksiyonun ikinci argümanıdır ve 12 byte'ın en üst noktasında bulunur. (4 talimatın işaretçisi, 4 birinci argüman ve 4 ikinci argüman için.)
Bir fonksiyonu terketmek iki aşamada yapılır.İlk olarak,çevre fonksiyon için temizlenmelidir (örneğin %ebp ve %eip yerleştirmek çağırmadan önce). Yığıtı kontrol etmemiz fonksiyon için gerekli bilgiyi almak için gereklidir.
Birinci adım fonksiyon ile birlikte atılmış oldu. :
Bir sonraki fonksiyon ile yapılacak olan çağrının yer tutması ve yığıtın yer teşkil etmesidir.
Bir önceki örneği taşıyoruz toto() fonksiyonu ilgili olan.
Burda başlangıç koşullarını açıklıyoruz.Çağrıdan önce %ebp adreste yer alır. X ve %esp, Y adresinde yer alır. Buradan fonksiyon argumanlarını biir kenara toplayarak %eip'i ve %ebp'i kaydettik ve yerel değişkenlerimiz için bir kısım yer ayırdık.Bir sonraki çalıştırılan talimat leave olacaktır.
Birincisi %esp ve %ebp'i alır.İkincisi %ebp kayıdında yer alanı yığıtın en üstüne yerleştirir. Sadece bir talimatta (leave), yığıt başlangıçsız olabilir.
ret talimatı %eip'ı saklar.Çağrı fonksiyonunun işlemi bu yolla başlar.Bunun için %eip'teki yığıtı kaldırabiliriz.
Fonksiyonun argümanları yığıtta saklanana kadar başlangıç koşullarına dönmeyeceğiz. Onların yerlerini ddeğiştirme bir sonraki talimat olacak. Z+5 ile tanımlanan adres %eip'ın içinde yer alır.
Parametrelerin yığıt haline getirilmesi çağrı fonksiyonunun içerisinde yapılır. Bu zıt diyagramda ayıraçlar yardımıyla çağrı fonksiyonda bulunan talimatların arasında ve add 0x8 ile çağrı fonksiyonunundaki %esp yardımıyla örneklenmiştir. Bu talimat %esp'ı yığıtın en üst noktasına verir toto() fonksiyon parametresinin kullanabildiği kadar kullanarak.%ebp ve %esp kayıtları şimdi olayın içinde yer alırlar.Diğer yandan %eip talimat kaydının yeri değişir.
gdb Makina kodunu almayı sağlar main() ve toto() fonksiyonlarına karşılık gelir :
Talimatlar (renk olmaksızın) bizim program talimatlarımıza karşılık gelir.
Bazen yığıtlarda varolan işlemlere müdehale etmek mümkündür.Bunu yapmanın yolu fonksiyonun geri dönüş adresinin üzerine yeniden yazmaktır ve keyfi kodlar ile uygulama çalışması yapmaktır.Bu özellikle kırıcılar için ilginçtir eğer uygulama kullanıcıdan farklı bir ID 'de çalışıyorsa.Bu tip bir hata bir bakıma tehlikeli olabilir eğer uygulama bir döküman okumaysa ve başka bir kullanıcı tarafından başlatılabiliyorsa.
Sonraki makalelerde talimatların uygulanmasındaki mekanizm üzerine konuşacağız. Burda biz kod üzerine çalışıyoruz ki bunu ana uygulama üzerinden çalıştırmayı istiyoruz. En basit çözüm kabuğu çalıştırmak için bir miktar koda sahip olmaktır.Okuyucu kendini diğer diğer uygulamalar için örneğin /etc/passwd dosyasının haklarını değiştirme konusunda eğitebilir.Bazı nedenlerden dolayı ki bunlar gelecekte açıkça görülecektir,bu program Makine dilinde yapılmak zorundadır.Bu tip küçük programlar kabuğu çalıştırma kaibliyitine sahiptir ve bunlara genellikle shellcode adı verilir.
Örneklerde Aleph One'ın makalesinden esinlenilmiştir "Smashing the Stack for Fun and Profit" Phrack magazin No:49.
Kabuk kodunun amacı kabuğu çalıştırmaktır.Bir sonraki C programı bunu örneklemektedir :
Fonksiyon kümelerinin arasında kabuğa çağrı gönderme kabiliyeti vardır. Birçok sebep execve()'ın kulllanışını doğrular.İlk olarak, O bir doğru sistem çağrısıdır ve diğer exec() ailesinden olan fonksiyonlara benzemez.Bunlar GlibC kütüphanesinin fonksiyonlarıdır ve execve()'den inşa edilmişlerdir.Bu kadar kayıt tanımlaması ve onların içerikleri üzerine konuşmak yeterlidir.
Bundan başka eğer execve() başarılırsa çağrı programı (burda ana uygulamadır) yeni programın çalıştırılabilir koduyla değiştirilir. execve() çağrısı başarısızlığa uğrsdığında program uygulaması devam eder.Örneğimizde kod hareket uygulamasının tam ortasındac yer alır. Uygulama ile devam etmek anlamsız ve hatta zaralı dahi olabilir.Uygulama ayrıca olabildiği ölçüde hızlı olmak zorundadır. return (0) programdan çıkma izni verir ve sadece bu talimat main() fonksiyonundan çağrıldığı zaman bu beklendiği gibi burada olmayacaktır.
exit() yine bir kütüphane fonksiyonudur.Yeni bir değişiklik bizim sisteme daha yakın olmamızı sağladı :
Şimdi programımızı Makine dilindeki karşılığı ile birleştirme zamanımız geldi.
Biz gcc ve gdb'yi Makine dili talimatlarını programın karşılıklarını alırken kullanıyoruz.Gelin shellcode3.c'u derleyelim (debug opsiyonu yardımıyla). (-g) programı bütünleme açısından normalde paylaşılmış kütüphanelerde bulunur.Şimdi _exexve() ve _exit()'yi anlamak için bilgiye ihtiyacımız vardır.
Sonra gdb ile bizim fonksiyonlarımızın Makine karşılıklarına bakacağız. Bu Linux içindir.
Şimdi hafızanın içeriğini bu adresten açıklamaya çalışalım :
Şimdi string'in ne olduğunu gördük. execve() ve _exit() fonksiyonlarına bakalım :
Çekirdek çağrısı 0x80 üzerinden 0x804d9d0 adresinde execve() için ve 0x804d99b adresinde _exit() için yapılır.Bu giriş noktası alışıldıktır. execve()'a baktığımızda onun 0x0B değerinin olduğunu ve _exit()'nun 0x01 değerinin olduğunu görürüz.
Bu fonksiyonların analizi Makine talimatlarıdır ve bunların kullanılışı parametreler yardımıyla sağlanırlar :
%ebx kaydı string adresini tutar ve bu adresler komutları ifade eder çalıştırmak için ;
%ecx kaydı argumanların adreslerini tutar.İlk arguman programın adı olmak zorundadır ve başka hiçbir seye ihtiyacımız yoktur ;
%edx kaydı array adreslerini tutar ve programın çevresinin harekete geçmesini sağlar.Program örneğimizi saklamak için boş bir çevre kullanacağız : Bu NULL işaretçisidir.
_exit() fonksiyonu çalışmayı durdurur ve uygulama kodunu genellikle kabuğa geri gönderir ve %ebx register içerisinde tutar ;
"/bin/sh" string'ine ihtiyacımız vardır, bir işaretçi bu string'e ve NULL işaretçisine. execve() çağrısından sonra olası bir veriyi sunabiliriz.Bir array inşa etmek için bir işaretçiyle birlikte /bin/sh string'ine NULL işaretçisini takiben, %ebx string'i işaret edecektir.
Kabuk kodu genellikle savunması zor programın komut satırı üzerinden arguman ile bir çevre değişkeni veya yazılmış bir string ile yerleştirilebilir.Herneyse, kabuk kodu oluşturulduğunda kullanacağı adresi bilmiyoruz.Bununla birlikte "/bin/sh" string'inin adresini bilmemiz gerekmektedir.Küçük bir oyun bunu öğrenmemizi sağlayabilir.
call talimatını çağırırken CPU geri dönüş adresini yığıtta saklar ve bu adres call talimatını takip eder. Genellikle, bir sonraki adım yığıt bölümünde saklanmaktadır (özellikle %ebp kaydı ile birlikte push %ebptalimatı) Geri dönüş adresini almak için subroutine içine girildiğinde pop talimatının yığıt halinden çıkarılması yeterlidir.Elbette "/bin/sh" string'ini saklamamız gereklidir :
Elbette subroutine gerçek değildir: execve() çağrısı başarır ve uygulama kabukla yer değiştirir veya başarısızlığa uğrar ve _exit() fonksiyonu programı sonlandırır. %esi kaydı bize "/bin/sh" string adresini verir.Daha sonra bir array inşa etmek string'ten sonra getirmekle mümkündür :
6 diyagramı veri alanını gösterir :
Savunulması zor fonksiyonlar genellikle string yönetimi ile ilgili olanlar örneğin strcpy()'dir.Hedef uygulamanın tam ortasına kodu yerleştirmek için kabuk kodu string şeklinde kopya edilmesi gereklidir.Bununla birlikte bu kopyalar null karakterini bulur bulmaz hemen duracaktır.Daha sonra bizim kodumuz olmayacaktır.Birkaç oyun kullanarak null byte'larını yazmayı engelleyebiliriz.Örneğin talimat
Bu örnek null byte'ının kullanılışını gösteriyor.Bununla birlikte bazı talimatların heksedesimale çevirileri ortaya çıkmaktadır.Örneğin _exit(0) sistem çağrısı ve diğerleri arasında ayrım yapmakm için %eax kayıt değerinin 1 olması gereklidir.
Diğer yandan "/bin/sh" string'i null byte ile son bulması gerekmektedir.Biz kabuk kodunu oluştururken bir tane koyabiliriz fakat bu programın içine konacak mekanizmaya bağlıdır.Bu null byte son uygulamada görünmeyebilir :
Şimdi kabuk kodunu oluşturmak için herşeye sahibiz :
"gcc -o shellcode4 shellcode4.c" ile kod derlenir. "objdump --disassemble shellcode4" komutu yardımıyla bizim binary'imiz (ikili tabanımız) daha fazla null byte tutmazlar :
Veri 80483c1 adresinden sonra gelir ve talimatları ifade etmez, fakat "/bin/sh" string karakterleri (in hexadécimal, sıra 2f 62 69 6e 2f 73 68 00)ve rasgele byte'lar.Kod başka sıfır tutmaz sadece null karakterini tutar string'in sonunda 80483c8.
Şimdi programımızı test edelim :
Ooops!Çok iyi bir sonuç değil.Eğer bit şeklinde düşünürsek hafıza alanınındaki main() fonksiyonu buluruz.(örneğin text alanından bahsedildimakalenin başında) sadece-oku.Kabuk kodu bunu değiştiremez.Ne yaapabiliriz kabuk kodunu test etmek için?Sadece-oku problemini çözmek için kabuk kodu veri alanına konulması gereklidir. Gelin bir array koyalım global değişken gibi davranan.Bir başka oyun da kabuk kodunu çalıştırabilsin main() fonksiyonu ile yığıtta bulunan geri dönüşüm adresini değiştirelim.Unutmayın ki main fonksiyonu bir "standard" rutindirve kodun bir parçası olarak tanımlanır.Geri dönüş adresi karakterlerin array'leri yığıtın en altında yer aldığı zaman tekrar yazılır.
Hatta shellcode5 programını da yükleyelim.UID root kuralım ve kabuğu dataileb kontrol edelim :
Bu kabuk kodu bir bakımdan sınırlıdır (evet, kötü değil biraz byte ile).Örneğin,test programımız şöyle olursa :
uygulamanın UID etkilerini sabitledik daha önceki makalede belirttiğimiz gibi.Şimdi kabuk ayrıcalıksız çalışabilir :
Bununla birlikte seteuid(getuid()) talimatı koruma için çok etkili değildir. setuid(0); kaydı kabuk kodunun başlangıcına eştir doğru linkleri EUID'ye almak için :
Bunu bizim daha önceki kabuk koduna indirgediğimizde örneğimiz :
Şimdi nasıl çalıştığını kontrol edelim :
Son örnekte görüldüğü gibi kabuğa fonksiyon eklemek mümkündür. ,örneğin dizinden çıkmak için chroot() fonksiyonu kullanmak gereklidir veye soket kullanarak uzaktan kabuğa ulaşmak gereklidir.
Bu tip değişiklikler bazen bazı byte'ların değerine adapte olmayı ima eder. :
Yaklaşık 40 byte program yazdık ve bunlar harici komutlarla çalışabilirler. Son örneklerimiz yığıtların nasıl parçalanabildiği konusunda fikir vermektedirler.Bu mekanizma üzerindeki ayrıntılar bir sonraki makalede yer almaktadır...
en -> tr Coşkun Demirboğa