Advanced Coding / Buffer Overflow Exploit -2

[ - Arabellek Taşma İstismarları (BOFE)/ Kapsamlı Bir İnceleme - Bölüm 0x02 - ]

İkinci Bölüme Giriş


Bu belge daha pek çok bölümden oluşacak bir yazıdisinin ikinci bölümü olduğundan öncelikle birinci bölümün okunmuş ve anlaşılmış olduğunu kabul ediyorum (Her türlü soru için bana e-posta atabilirsiniz.).

Birinci bölümde arabellek taşma istismarlarını anlamak için şart olan yığıt yapısı analizini yapmıştık (ki kendi adıma çok sıkıcı bir işti! Smile. Arabellek'lerin en önemlisi olan yığıtı anlattık. Şimdi başlıkta geçen "taşma" olayına giriyoruz. Biraz daha eğlenceli bir bölüm olacağını garanti ediyorum!

Hemen bir uyarıda bulunayım. Birinci bölümde olayın temeline hakim olmayanlara, hatta unix kullanımına pek aşina olmayanlara da hitap edebilmek için basit ve bol açıklamalı bir dil kullandıysam da artık onlar da kıvama gelmiş olduklarına göre bu bölümden itibaren yavaş yavaş dilimiz seviye atlayacak.

Bu bölümde amacım sizlere bol bol kod grepletmek! Her zaman olduğu gibi tavsiyelerim ise :

1. Ortamı karartın.
2. En sıkı mp3 listenizi yükleyin.
3. Nescafe'yi hazır bulundurun.
4. Renkleri siyah üzerinde açık yeşile çevirin.
5. Monitörünüzün ayarıyla oynamayın! 


Taşırma Nedir?


Taşırma bellekte ayrılan yere, ayrılmış olan alandan daha büyük bir bilgi girdiğimizde, bu bilginin kendisine ayrılmış alandan taşarak diğer alanların da üzerine yazması olarak özetlenebilir. Bunun gerçekleşebilmesi için taşma kontrolünün yapılmamış olması gerekiyor elbette.

Taşırma olayına en çok C programlarında rastlanır. Çünkü C dili makine-diline yakın ve esnek olabilmek için taşma kontrolünü otomatik olarak yapmaz (Derleyiciler normal olarak kontrol kodu eklemezler demek daha doğru). Aslında bu programcıya gayet esneklik sağlayan ve genel olarak beğenilen bir özelliktir. Kontrolü derleyicinin yapması yerine, gerekli yerlerde programcının yapması tercih edilir. Fakat programcıların büyük bir çoğunluğu bu tür kontrol kodlarını eklemeyi atlar veya da buna gerek görmez. Sonuç olarak 1980'lerden beri yaygın olarak bilinen bu durum mevcut programların potansiyel olarak hemen hepsinde bulunabilir.

Taşırma zayıflık (vulnerability) sınıfına giren ciddi bir kodlama hatasıdır. Burada zayıflık teriminin anlamı, kodun belirli tür saldırılara potansiyel olarak açık olduğudur. Bu açıklık uygun bir "istismar" (exploit) kodu yazılması ile bir işe yarar hale getirilebilir.

"That vulnerability is completely theoretical! (Bu zayıflık tamamen teorik!)"
-- Micro$oft


Micro$oft'un kendini savunma mantığına diyecek yok doğrusu! Zayıflıkların teorik olup olmaması diye bir şey yoktur; fakat teorik olarak bütün zayıflıklar istismar edilebilir!

Taşma zayıflıkları çeşitli biçim ve yöntemlerde olabilir. Fakat en sık rastlanılanı arabellek taşmalarıdır. Arabellek taşmaları da kendi içlerinde kategorilere ayrılır. Biz en kolay ve yaygın olanlardan başlayarak, daha zor ve ender olanlara doğru ilerleyeceğiz. Hatta yaygın olduğu halde istismar edilebileceği bilinmediği için henüz istismar kodları bulunmayan zayıflıklara kadar bu belgeyi taşımayı düşünüyorum. [Aslında basit "exploit"ler yazmak için neredeyse hiçbir altyapı gerekmez. Benim asıl hedef kitlem mevcut istismarlarla yetinmeyerek yeni yöntemler geliştirecek altyapıya sahip olmak isteyenler. Bu belgedeki bilgiler sizleri böyle bir noktaya götürme hedefi dikkati alınarak oluşturuldu!]

Bu kadar sosyal-muhabbet yeter sanırım. Arabellek taşma zayıflıklarının farklı kategorileri olduğundan bahsetmiştik. En sık rastlanan zayıflık kategorisi ise yığıt-tabanlı taşmalardır. Bu tür aynı zamanda en tehlikeli olanıdır da! Neden tehlikeli olduğunu ileride göreceğiz.


Yığıt-Tabanlı Taşma Mantığı


Birinci bölümdeki son kodumuzu biraz basitleştirerek işe başlayalım:

// Ornek4.c
// ~~~~~~~~
void f2()
{
char b[11];
}

void f1()
{
f2();
}

int main()
{
f1();

return 0;
}



Önceki bilgilerimize dayanarak bu kodun oluşturduğu yığıtı çizelim:

Adres Erişim Değer

(Gerçek) (main) (f1) (f2)
============= ÜST ADRES
N ebp+12 --- --- | argv |
|-----------|
N-04 ebp+8 --- --- | argc |
|-----------|
N-08 ebp+4 --- --- | Ret_main |
|-----------|
N-12 ebp --- --- | ebp [xxxx]|
|-----------|
N-16 ebp-4 --- --- | (özel) |
|-----------|
N-20 ebp-8 --- --- | (özel) |
|-----------| --------
N-24 ebp+4 --- | Ret_f1 |
|-----------|
N-28 ebp --- | ebp [N-12]|
|-----------|
N-32 ebp-4 --- | (özel) |
|-----------|
N-36 ebp-8 --- | (özel) |
|-----------| --------
N-40 ebp+4 | Ret_f2 |
|-----------|
N-44 ebp | ebp [N-28]|
|-----------|
N-48 ebp-8 | b[8]-b[10]|
|-----------|
N-52 ebp-12 | b[4]-b[7] |
|-----------|
N-56 ebp-16 | b[0]-b[3] |
|-----------|
N-60 ebp-20 | --- |
|-----------|
N-64 ebp-24 | (özel) |
|-----------|
N-68 ebp-28 | (özel) |
============= ALT ADRES



Bu yığıt yapısında anlaşılmayacak bir şey olmadığını düşünüyorum. Programda bir tane arabellek (buffer) var. Yukarıdaki yapıdan bu arabelleğin "N-56" adresinden başladığını görebiliyoruz. "f2" fonksiyonunun dönüş adresinin de "N-40" adresinde olduğu açıktır.

"b" isimli arabelleğimize 11'den fazla karakter yazarsak bulunduğu yerden taşacaktır. 13. karakter "N-44" adresindeki ebp'nin ilk sekizlisinin üzerine yazacaktır. Aynı şekilde 14.,15. vd 16. karakterler de ebp'yi değiştirir. İşte bu durum açıkça taşma olayıdır. Asıl önemli olan 17.-20. karakterlerin Ret_f2'nin üzerine yazacak olması! Böylece dönüş değerini değiştirebiliriz! Bu fikri hemen denemeliyiz. Yapacağımız şey "b" değişkeninin adresini öğrenerek 16-sekizli sonrasından itibaren 4-sekizliyi (ki bu dönüş adresinin olduğu yer oluyor) değiştirmek. Bunu yapabilmek için b'nin adresini alıp 16-sekizli sonrasını bularak bir "long" göstergeye çevirmeliyiz öncelikle. Yani "(long*)(b+16)". "long" gösterge 4-sekizli değerinde olan dönüş adresinin değerini tümden değiştirebilmemizi sağlayacak. Örneğin dönüş adresini 0xdeadbeef olarak değiştirelim:

// Ornek5.c
// ~~~~~~~~
void f2()
{
char b[11];
*(long*)(b+16) = 0xdeadbeef;
}

void f1()
{
f2();
}

int main()
{
f1();

return 0;
}



Şimdi program çalıştığı zaman önce f1 fonksiyonuna girecek ardından da f2'ye. Fakat f2'den dönmeye çalıştığı zaman gerçekten kaldığı adrese değil de 0xdeadbeef adresine dönecek! Sonuçta ne olduğunu görelim:

$ gcc -o ornek4 ornek4.c
$ ./ornek4
Parçalama arızası (core dumped)
$



İngilizce sistemlerde bu hata mesajının orjinali "Segmentation Fault (core dumped)" olacaktır. Niçin hata aldık? Çünkü yeni dönüş adresi programın adres-uzayının dışında (adres-uzayı olayına girmeyeceğim. Bunun için "Segment" yazmaçlarının kullanımı ve sanal-bellek ile ilgili bir belge okumanızı öneririm).

Başka şekilde de yığıttan taşma yapabilirdik:

// Ornek6.c
// ~~~~~~~~
void f2()
{
char b[11];

strcpy(b,"AAAAAAAAAAAAAAAAAAAA");
}

void f1()
{
f2();
}

int main()
{
f1();

return 0;
}



Bu durumda da son dört "A" harfi dönüş adresini "AAAA" olarak değiştiriyor ki bu da ASCII kodu olarak 0x41414141 demektir (ki yine proses adres-uzayının dışındadır. Aslında atmasyon bir adresin proses adres-uzayı içerisinde olması ihtimali piyango çıkması gibi bir şeydir!).

$ gcc -o ornek4 ornek4.c
$ ./ornek4
Parçalama arızası (core dumped)
$



Daha çok taşma yaptığımız takdirde f1 fonksiyonunun dönüş adresini ve hatta "main"den dönüş adresini bile değiştirebiliriz. Tabi örneğin "main"in dönüş adresini, f1 ve f2'nin adreslerini de değiştirmeden değiştiremeyiz. Çünkü yol onlardan geçiyor! Bu durumda sonraki dönüşlerin aktif olması için gerideki adresleri tekrar düzeltmeliyiz.

strcpy() fonksiyonunun "b" dizisi gibi karakter dizileri için sınır kontrolü yapmadığını gördük! Bu durumda strcpy() gibi bazı karakter-dizisi fonksiyonlarının tehlikeli olduğunu söyleyebiliriz:

$ man strcpy
...
...
...

BUGS
If the destination string of a strcpy() is not large
enough (that is, if the programmer was stupid/lazy, and
failed to check the size before copying) then anything
might happen. Overflowing fixed length strings is a
favourite cracker technique.
...
...
:q
$
$ man gets
...
...
...
BUGS
Never use gets(). Because it is impossible to tell with-
out knowing the data in advance how many characters gets()
will read, and because gets() will continue to store char-
acters past the end of the buffer, it is extremely danger-
ous to use. It has been used to break computer security.
Use fgets() instead.
...
...
:q



Diğer tehlikeli durumlar döngü içerisinde adet belirtmeden karakter kopyalamak, hatta kontrolsüz şekilde scanf() kullanmaktır. Şimdi bunun programcı sınır değeri aşmayan bir değer kopyaladığı sürece ne sorun yarattığını sorabilirsiniz. Bu her zaman programcının kontrolünde değildir. Aşağıdaki örneğe bakalım:

// Ornek7.c
// ~~~~~~~~
int main(int argc, char* argv[])
{
char hostname[128];

if(argc==1)
puts("kullanım: ornek7 hostadı");
else
strcpy(hostname,argv[1]);

return 0;
}



Bu kodda strcpy() fonksiyonunu örnekliyoruz ve kullanıcıdan bir hostadı alıyoruz (diyelim ki bir Ping programı).

$ gcc -o ornek7 ornek7.c
$ ./ornek7
kullanım: ornek7 hostadı
$ ./ornek7 core.gen.tr
$



Diyelim ki gayet guzel çalışan bir Ping programı bu. Yalnız bir kusuru var. Hostadı için sınır kontrolü yapmıyor:

$ ./ornek7 burayayuzyirmisekizigececekkadarcokkarakteryaziyor uzv
core.gen.tr...core.gen.tr...core.gen.tr...core.gen .tr...core.gen.tr
...core.gen.tr...core.gen.tr...core.gen.tr...
Parçalama arızası (core dumped)
$



Sanırım bu örnek kullanıcılara fazla güvenmemek gerektiğini gösteriyor! Aynı durumu başka şekilde örnekleyelim:

// Ornek8.c
// ~~~~~~~~
int main()
{
char isim[128];

puts("Adınız [maks.127]: ");
scanf("%s",isim);

return 0;
}



scanf() fonksiyonunun boşluk, tab v.b. karakterlere rastlayınca durduğunu unutmayalım (ki bu ilerde başımızı çok ağrıtacak). Eğer bir program size "maksimum şu kadar", "en fazla bu kadar" falan gibi uyarılarda bulunuyorlarsa ilk deneyeceğiniz şey o maksimumu aşacak bir şeyler denemek ve sonucun görmektir! (Tanenbaum hocamız da Minix kitabının sonlarında "bir cracker öncelikle kullanım kılavuzları ve yardım dosyalarını 'Aman sakın X yapmayın' tarzında cümleler için arayacaktır; ve ardından da X'in olası her türünü deneyecektir!" gibi bir uyarıda bulunuyordu).

$ gcc -o ornek8 ornek8.c
$ ./ornek8
Adınız [maks.127]:
Çağıl
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 1234567
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 12345678
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 123456789
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 1234567890
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 1234567801
$
$ ./ornek8
Adınız [maks.127]:
12345678901234567890123456789012345678901234567890 12345678901234567890
12345678901234567890123456789012345678901234567890 12345678012
Parçalama arızası (core dumped)
$ Smile



Web sayfasını fazla genişletmemesi için ortasına bir "Enter" yerleştirdim. Yukarıdaki satır aslında iki değil tek satırdır. Kaç karakter yazdığımızı anlayabilmek için sayıları kullandım. Uzun arabellekleri doldurabilmek icin fare ile "1234567890" kısmını seçerek orta tuş (ya da iki-tuşa aynı anda basma yöntemi) ile yapıştırabiliriz. Bu sırada "klik!"lerimizi sayıp kaç karakter bastığımızı anlayabiliriz.

SORU: Arabelleği 128 uzunluğunda açtık. Dönüş adresinin örneğin sadece bir sekizlisini değiştirmek için bir karakter daha ekleyerek 129 uzunluğunda bir değer neden SegFault (Parçalama Arızası) vermedi?

YANIT: Çünkü (%ebp)'nin değeri var yığıtta öncelikle. Dönüş adresi daha sonra geliyor. Tamam bu basitti :P


SORU: Peki (%ebp) icin 4-sekizli daha eklediğimizde 129+4=133 ediyor. Gerçekten de 133 karakter SegFault veriyor. Ama 132 karakter yazdığımızda da (yukarıda en son olan) SegFault alıyoruz. Neden?

YANIT: Çünkü girdiğimiz değer bir karakter-dizisi olduğundan sonuna sonlandırıcı bir NULL (0x00) karakteri otomatik olarak scanf() fonksiyonu tarafından ekleniyor ve dizimiz 133 karakter oluyor.


Programın Akışını Değiştirmek


Dönüş adresini böyle rastgele değiştirerek programlara SegFault çakmak eğlenceli olabilir ama hayat böyle geçmez! Dönüş adresini daha itinayla değiştirmek gerekiyor. Örneğin aşağıdaki programı düşünelim:

// Ornek9.c
// ~~~~~~~~
void f1()
{
int hack;
}

int main()
{
int x = 1;

f1();
x++;

printf("2 olmasi gerektigini bildigimiz x=%d.\n",x);

return 0;
}



$ gcc -o ornek9 ornek9.c
$ ./ornek9
2 olmasi gerektigini bildigimiz x=2.
$



Bu program çalışması gerektiği gibi çalıştığına hiç kuşku yok herhalde. Normal olarak f1() fonksiyonu içerisindeki bir kodun main() içindeki çalışmayı etkilememesi gerekir. Fakat f1() içinde haylaz bir satır bulunduralım:

// Ornek10.c
// ~~~~~~~~
void f1()
{
int hack;
*(((long*)(&hack))+2)+=3;
}

int main()
{
int x = 1;

f1();
x++;

printf("2 olmasi gerektigini bildigimiz x=%d.\n",x);

return 0;
}



Eklediğimiz bu acayip satır sonucu nasıl değiştirebilir ki?

$ gcc -o ornek10 ornek10.c
$ ./ornek10
2 olmasi gerektigini bildigimiz x=1.
$



Garip bir şeyler var! Şimdi olay aslında çok basit. "gdb"mize dönelim bakalım:

$ gdb ornek10
GNU gdb...
(gdb) disass main
Dump of assembler code for function main:
main: push %ebp
main+1: mov %esp,%ebp
main+3: sub $0x18,%esp
main+6: movl $0x1,0xfffffffc(%ebp)
main+13: call 0x8048400 (f1)
main+18: incl 0xfffffffc(%ebp)
main+21: add $0xfffffff8,%esp
main+24: mov 0xfffffffc(%ebp),%eax
main+27: push %eax
main+28: push $0x80484e0
main+33: call 0x8048300 (printf)
main+38: add $0x10,%esp
main+41: xor %eax,%eax
main+43: jmp 0x8048440 (main+48)
main+45: lea 0x0(%esi),%esi
main+48: mov %ebp,%esp
main+50: pop %ebp
main+51: ret
main+52: lea 0x0(%esi),%esi
main+58: lea 0x0(%edi),%edi
End of assembler dump.
(gdb) disass f1
Dump of assembler code for function f1:
f1: push %ebp
f1+1: mov %esp,%ebp
f1+3: sub $0x18,%esp
f1+6: addl $0x3,0x4(%ebp)
f1+10: mov %ebp,%esp
f1+12: pop %ebp
f1+13: ret
f1+14: mov %esi,%esi
End of assembler dump.
(gdb) q



"main+13" satırında f1 fonksiyonunu çağırıyoruz. "call" komutu çalıştırıldığı anda dönüş adresinin yığıta "push" edildiğini hatırlıyorsunuz değil mi? Bu kaydedilen dönüş adresi doğal olarak "main+18" adresini gösterecektir ki program fonksiyon dönüşü çalışmasına devam edebilsin. "x++;" satırına karşılık gelen çevirici kodu da bu "main+18" adresinde bulunmakta. Bu satırı atlatmak istersek bir sonraki komut "main+21" adresinden başladığından, dönüş adresini 3-sekizli arttırmalıyız. (Bu satırı atlatınca x'in 1 olarak kalacağı açık herhalde!).

f1()'in çıktısına baktığımızda beklediğimiz gibi 0x18-sekizlik alanın yığıtta ayrılmış olduğunu görüyoruz. Bizim "hack" adlı değişkenimiz de yığıtta bu 0x18'lik alanın en yukarısındaki dörtlükte yer alacaktır. Dolayısı ile "hack" değişkeninin bulunduğu adres ile dönüş adresinin bulunduğu adres arasında yalnızca "push" edilmiş olan "ebp" değeri bulunmaktadır. Bu da basit bir matematikle: (hack'in adresi)+8=(dönüş adresinin adresi). Bunu da C'de "((long*)(&hack))+1" şeklinde gösterebiliriz. "long" göstergeye çevirdiğimizden +2 ifadesi "2-long" yani 8-sekizli ekleyecektir. Şimdi bu adresteki değeri 3-sekizli arttırmak için "*(((long*)(&hack))+2)+=3;" yazmamız yeterli!

Programın akışını değiştirmek bir şifre sorma adımını atlatmak gibi çok tehlikeli amaçlarla da kullanılabilir. Yalnız programın kaynak koduna normalde erişimimiz olmayacağı için dönüş adresini yığıtı taşıran karakter dizisinin içine doğrudan yazmalıyız ki 1) İstediğimiz yeni dönüş adresinin tam değerini bulmak çok zordur. 2) Bu adres ekrana karakter-karakter basılamayacak karakterler içerebilir (içerir!). Bu tür sorunları çözmekle ileriki bölümlerde uğraşacağız.

Bu bölümde istediğimden fazla laf yaptım. Sonraki bölümlerde daha az laf, daha çok kodlama yapacağız. Çünkü daha önce de belirttiğim gibi artık Unix yönetimi ve programlaması konusunda zayıf olanlarınız bile belirli bir seviyeye gelmiş olmalılar. En önemlisi de anlamadığınız bir yer olduğunda, artık çoğunuz gerekli araştırmayı kendi kendine yapacak, man sayfalarını karıştıracak, kodları grepleyecek, ve bu işin içinde olan hepimizin yaptığı gibi eninde sonunda geceleri uykusuz kalma pahasına onu anlayacak ve içinizi yiyip bitiren o bilmemek karabasanından sizi kurtaracak becerileri edinmiş durumdasınız artık! Üçüncü bölümde artık "shellcode" denen olayla birlikte hızlanıyoruz!

Kaynak: Çağıl Şeker
Tarih:
Hit: 3098
Yazar: Omega

Taglar: advanced coding / buffer overflow exploit -1


Yorumlar


Siftahı yapan siz olun
Yorum yapabilmek için üye girişi yapmalısınız.