Advanced Coding / Buffer Overflow Exploit -1

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

Giriş



Bu yazı dizisinde anlatacağım teknik C programlarının istismar edilmesiyle kullanıcı haklarımız yeterli olmasa dahi istediğimiz kodu çalıştırabilmemizi sağlayan ve BOFE olarak bilinen bir tekniktir. Bu teknikle en basitinden programın çalıştığı sistemin bütünlüğünü bozabilir, en iyisinde ise kendimize bir "rootshell" yani süper kullanıcı haklarına sahip bir komut-istemcisi açabiliriz! Ünlü Kevin Mitnick olayı dahil bilinen (ve bilinmeyen) hack olaylarının pek çoğu da bu iyi bilinen tekniğin zeki ve yaratıcı şekilde kullanılmasıyla başarılmıştır. Dolayısıyla güvenlik uzmanları için bu tekniğin bilinmesi son derece önemlidir. İleriki bölümlerde bu tekniği ayrıntılarıyla inceleyeceğim.

Bu belge son yıllarda sıkça sözü edilen "Buffer Overflow Exploit (BOFE)" ve "Buffer Overflow Vulnerability" terimleri ile ilgilidir. "Vulnerability" kelimesi için Türkçe "Zayıflık" kelimesini kullanacağım. "Exploit" kelimesini ise en yakın "İstismar" olarak tercüme edebildim. ("Sömürü" kulağa çok sadistçe geliyordu!). Kullanacağım diğer terimler ise "Buffer" için "Arabellek", "Stack" için "Yığıt", "Register" için "Yazmaç", "Assembly Language" için "Çevirici-Dili", ve "Pointer" için "Gösterge" olacak (ki TBD'nin sözlüğünden aldım).

Bu belgede arabellek taşma istismarlarını, "Linux/Intel/gcc" mimarisini temel alarak, ardındaki teknik mantıktan başlayarak ele alacağım. Öncelikle temel bilgileri aktararak, ardından basit istismarları inceleyecek, daha sonra filtre-aşma, chroot'tan kurtarma ve daemon'dan shell sunucusu alma, SolarDesigner'ın çalıştırılamaz yığıt yamasını aşma, gösterge yönlendirme gibi daha teknik konulara yönelecek, ve son olarak da (hala yazacak gücüm kalırsa) ELF formatı, dynamic-linking ve heap-overflows gibi zorlayıcı konuları BOFE ile ilişkileri kapsamında yazacağım. Ayrıca x86 mimarisi yanında, MIPS, Motorola, Sparc işleyicileri ile Solaris, IRIX, AIX, HP/UX, Windows NT mimarileri üzerinde de çalışacağız. Bu sırada da bol bol örnekle ilerleyeceğim. Çünkü kendi adıma, ben örneklerle çok daha iyi anlıyorum. Örneklerin mutlaka denenerek gözlenmesini öneriyorum.


Bu Belge Kimlere Hitap Ediyor?


Öncelikle bu belge, Unix/Linux platformuna aşina, az çok C dili ile programlama yapmış, bilgisayar mimarisi ve işleyici çalışma prensipleri hakkında biraz bilgi sahibi olan ve artık bilgi düzeyini daha yaratıcı alanlara doğru taşımaya istekli kişilere hitap etmektir.

Gerekenler:

Unix türevi bir işletim sistemi, Intel veya AMD işlemci, "gcc" C derleyicisi, "gdb" veya eşdeğer bir inceleme aracı, standart C dili programlama bilgisi, en temel düzeyde çevirici-dili bilgisi, Intel yazmaçları hakkında temel bilgi, basit-düzey sistem yönetimi, tabi ki yaratıcı düşünebilme yeteneği ve olmazsa olmaz bol bol sıcak su ve Nescafe. Monitör başında sabahlayabilme yeteneğini saymıyorum bile Smile .


Yığıt Yapısı


Yığıt bir çok işletim sistemi tarafından yardımcı işlemler için kullanılan bir arabellektir. Yığıt ile ilgili işlemler Intel, Sparc ve Motorola tabanlı işleyiciler başta olmak üzere pek çok işleyici tarafından makina-dili düzeyinde desteklenir. Yığıt yapısı ve kullanımı mimariye göre son derece değişik olabilir. Ben başlangıçta Linux Kernel 2.4 işletim sistemi ve Intel işleyicilerini temel alacağım. Daha sonra diğer mimarilere giriş yapacağız.

Yığıt, Son-Giren-İlk-Çıkan (Last-in-First-out - LIFO) olarak bilinen bir liste türüdür. Bunun anlamı, yığıttan bir değer istediğimizde, yığıta son eklenen değeri alacağımızdır. Yığıt belleğin üst adres bölgesinde bulunur. Yığıtın uzunluğu dolayısıyla da tepe noktası değişkendir. Yığıt dinamik değerler için kullanılır ve alt bellek adreslerine doğru büyür. Yığıt alanı dışında önemli alanlar (segment'ler) VERİ ve KOD alanlarıdır. VERİ alanı statik değerler için kullanılır. KOD alanı ise programın kaynak kodlarının bulunduğu yalnız-okunur alandır. Buraya kadar her şey açıktır umarım.

Intel işlemcilerde bilmemiz gereken yazmaçlar EIP (kod-göstergeci), ESP (yığıt-göstergeci) ve EBP (taban-göstergeci)'dir. EIP çalıştırılacak kodun adresini ve ESP yığıtta en son eklenen değerin bulunduğu adresi gösterir. EBP genel amaçlı bir yazmaçtır ve genelde yığıt üzerinde belirli bir değeri göstermek için (ki buna çerçeve-göstergesi denir) derleyiciler tarafından kullanılır.

Belleğe erişim yalnızca bir sözcük(word)'ün katları şeklinde yapılabilir. Bu çok önemli. Intel'de bir sözcük 4-sekizli(bayt)'dir. Tekrarlıyorum; belleğe yalnızca 4-sekizli'nin katları şeklinde erişim yapabiliriz. Dolayısıyla belleği şematik olarak gösterirken 4-sekizli uzunluğunda satırlar kolaylık sağlamaktadır. (Anlaşılmayan bir şey olmamalı burada).

Burada bir uyarıda bulunmalıyım. Eski ve yeni Linux dağıtımları (daha doğrusu gcc'ler) yığıt kullanımı açısından önemli farklar içeriyorlar. Yeni dağıtımlar bir çeşit optimizasyon kullanıyorlar. Eski dağıtımlar daha düz ve anlaşılır kod üretirken, yenilerin kodları biraz daha gizemli hale gelmiş durumda. Bu gizemi ilerde inceleyeceğiz. Kafanıza takmayın şimdilik.


Basit Yığıt Yapısı


Eğer şimdiye kadar yalnızca üst düzey programlama ile ilgilendiyseniz işleyicinin "kaotik metal derinliklerinde" (iyi laf!) gerçekte neler olduğunu öğrenmek ilginizi çekebilir. Basit (gerçek anlamda basitWink bir C programını inceleyelim:

// Ornek1.c
// ~~~~~~~~
int main()
{
return 0;
}



Evet, bu program gerçekten de hiçbir şey yapmıyor. Aslında bir işe yarayan programları anlamak için, bir işe yaramayanları anlamak gerektiği evrenin sabit bir yasasıdır. Bunu incelemek için "gdb" aracını kullanacağım. ("Debugging" ile ilgili etrafta pek çok belge var, bu nedenle bu konuyu ayrıntılı anlatmıyorum)

psiXaos@psiXaos bofe $ cat /proc/version
Linux version 2.4.17-r3 (gentoo@core.gen.tr) (gcc version 2.95.3)
#2 Wed Feb 28 03:13:37 EET 2002
psiXaos@psiXaos bofe $ gcc -o Ornek1 Ornek1.c
psiXaos@psiXaos bofe $ gdb Ornek1
GNU gdb 5.1
Copyright 2001 Free Software Foundation, Inc.
GDB is free software...
(gdb) disass main
Dump of assembler code for function main:
0x80483d0 : push %ebp
0x80483d1 <main+1 />: mov %esp,%ebp
0x80483d3 <main+3 />: xor %eax,%eax
0x80483d5 <main+5 />: jmp 0x80483d7 <main+7 />
0x80483d7 <main+7 />: mov %ebp,%esp
0x80483d9 <main+9 />: pop %ebp
0x80483da <main+10 />: ret
0x80483db <main+11 />: nop
0x80483dc <main+12 />: lea 0x0(%esi,1),%esi
End of assembler dump.
(gdb) q



Değişik sistemlerde adresler ve komutlar hafifçe farklı olabilir ama temel mantık aynıdır (Aynı gibi gelmiyorsa ya çevirici-dili bilginiz yetersizdir, ya da tarihi bir makina kullanıyor olmalısınız!). gcc'nin çevirici çıktısı size biraz yabancı gelebilir (Özellikle de "Windoze" ya da NASM'la haşır neşir olmuşsanız.); çünkü gcc AT&T çevirici notasyonunu kullanmaktadır. Bu notasyonda yazmaçlar "%" işareti ile gösterilir ve işlemler soldan sağa okunur. Yani "mov %esp,%ebp" kodu %esp'nin içeriğini %ebp'ye aktarmaktadır.

push %ebp
mov %esp,%ebp



Bu overtür kod bir çok C derleyicisi tarafından standart olarak kullanılır ve PROLOG olarak adlandırılır. "push" komutu yığıta bir değer eklemek ve "pop" komutu yığıttan bir değer okumak için kullanılır ve bunlara türkçe bi karşılık bulmayacağım. %esp yazmacı yığıtın tepesini gösterir (Top of Stack - ToS) ve her push'la birlikte azaltılır; pop'la birlikte yeniden yükseltilir.

Bu durumda "push %ebp" komutu %ebp'nin değerini yığıta saklamakta ve ardından "mov %esp,%ebp" komutu yığıt yazmacının değerini (ToS) ebp'ye aktarmaktadır. Bu kodun amacı yığıta erişimde %ebp'ye göreli bir adres kullanmaktır. (Aslında %esp'ye göreli bir adres kullanmak de olasıdır fakat yığıt yazmacının değeri her pop ve push komutu ile değiştiğinden statik bir göreli adres hesaplamak zor olmaktadır ve bu yüzden derleyiciler tarafından tercih edilmez. ) Şimdi yığıtımız şu hali aldı:

Adres Değer

============= ÜST ADRES
N | %ebp | <--- [%esp] == [%ebp]
|-----------|
N-04 | |
=============



Yukarıdaki yığıt yapısı ilerisi için son derece önemli. Dikkat edin artık %ebp "kendi eski değerinin" saklandığı yığıt adresini gösteriyor. "%ebp" değerinin saklandığı bu yığıt adresi bir çeşit GMT'dir ve bütün yığıt erişimleri derleyiciler tarafından bu adrese göreli olarak yapılır.

Adresleme ile ilgili bir not:
Adres "N" değeri 4-sözcük uzunluğunda ise "N, N+1, N+2 ve N+3" adreslerini ifade eder! "%esp" son dolu yığıt adresini gösterdiğine göre (ilk boş adresi değil!) esp'nin değeri "N+4" ise, son bilgi "N+4, N+5, N+6 ve N+7" adreslerinde bulunmaktadır ve bir "push ebp" komutu ebp'nin 4-sözcük uzunluğundaki değerini "N" adresinden başlayarak "N, N+1, N+2 ve N+3" adreslerine yazacaktir. Bu bazen kafa karıştırabiliyor diye belirtiyorum.

xor %eax,%eax
jmp 0x80483d7



Bu tamamen derleyicinin eklediği gereksiz bir kod. Sizin sisteminizde bulunmayabilir. (gcc'ye -O3 gibi bir optimizasyon opsiyonu verirsek çıktı bu gibi gereksiz kodlardan kurtulur. Ama yığıt yapısını anlamamızı sağlayacak kodlar da bu optimizasyondan nasibini alacağından -O opsiyonunu aman sakın kullanmayın!)

mov %ebp,%esp
pop %ebp
ret



Bu kısım programın sonunda bulunur ve EPILOG olarak adlandırılır. "mov %ebp,%esp" ile %esp'nin değerini tekrar ilk haline, yani ebp'de sakladığımız eski haline getirerek yığıta eklenmiş değerleri geçersiz kılıyoruz. Çünkü artık yığıtın tepesi yukarıya taşınmış oluyor. Bu basit programda yığıta eklenmiş olan değerler bulunmadığından %esp'nin değeri zaten değişmeden kalıyor. Fakat basit olmayan programlarda arada yığıt işlemleri nedeni ile %esp değişecektir. "pop %ebp" komutu ile de önceden saklanmış olan %ebp'yi geri alıyoruz. "ret" ile işletim sistemine geri dönüyoruz.

Şimdi ikinci örneğimize bakalım:

// Ornek2.c
// ~~~~~~~~
int main(int argc, char* argv[])
{
int i=argc;
char** a=argv;
char* b=argv[0];
char* c=argv[1];

return 0;
}



Yığıt kullanımı örneklemek için program komut-satırı argümanlarını ve bir de yerel değişkeni işin içine kattık. Bu durumda yığıtın nasıl kullanıldığını anlamak için kodun içine argüman ya da değişkeni kullanan satırlar ekledim. Bakalım metal düzeyinde durum nasıl:

$ gcc -o Ornek2 Ornek2.c
$ gdb Ornek2
GNU gdb...
(gdb) disass main
Dump of assembler code for function main:
0x80483d0 : push %ebp
0x80483d1 <main+1 />: mov %esp,%ebp
0x80483d3 <main+3 />: sub $0x18,%esp
0x80483d6 <main+6 />: mov 0x8(%ebp),%eax
0x80483d9 <main+9 />: mov %eax,0xfffffffc(%ebp)
0x80483dc <main+12 />: mov 0xc(%ebp),%eax
0x80483df <main+15 />: mov %eax,0xfffffff8(%ebp)
0x80483e2 <main+18 />: mov 0xc(%ebp),%eax
0x80483e5 <main+21 />: mov (%eax),%edx
0x80483e7 <main+23 />: mov %edx,0xfffffff4(%ebp)
0x80483ea <main+26 />: mov 0xc(%ebp),%eax
0x80483ed <main+29 />: add $0x4,%eax
0x80483f0 <main+32 />: mov (%eax),%edx
0x80483f2 <main+34 />: mov %edx,0xfffffff0(%ebp)
0x80483f5 <main+37 />: xor %eax,%eax
0x80483f7 <main+39 />: jmp 0x8048400 <main+48 />
0x80483f9 <main+41 />: lea 0x0(%esi,1),%esi
0x8048400 <main+48 />: mov %ebp,%esp
0x8048402 <main+50 />: pop %ebp
0x8048403 <main+51 />: ret
0x8048404 <main+52 />: lea 0x0(%esi),%esi
0x804840a <main+58 />: lea 0x0(%edi),%edi
End of assembler dump.
(gdb)q



"0x8(%ebp)" ifadesi %ebp'nin gösterdiği yerden 8-sekizli sonrasıdır. Ama "0xfffffffc(%ebp)" ifadesi elbette ki o kadar uzakta değil, 0xfffffffc'nin yani ikili olarak "11111111111111111111111111111100" sayısının en ağırlıklı (en soldaki) biti "1", dolayısıyla bu negatif bir sayı olarak yorumlanmalıdır. Negatif sayıları normal gösterime çevirmek için 0xffffffff'den çıkarıp artı bir ekliyoruz: 0xffffffff - 0xfffffffc + 1 = 0x4 ; yani "-0x4(%ebp)", ki bu kod da %ebp'nin gösterdiği yerden 4-sekizli öncesidir. Şimdi çıktıyı biraz düzenleyip önümüze koyalım. Bakalım neler çıkarabileceğiz:

push %ebp # PROLOG
mov %esp,%ebp # PROLOG
sub $0x18,%esp # Yerel değişkenler
mov 0x8(%ebp),%eax #
mov %eax,-0x4(%ebp) # int i=argc;
mov 0xc(%ebp),%eax #
mov %eax,-0x8(%ebp) # char** a=argv;
mov 0xc(%ebp),%eax #
mov (%eax),%edx #
mov %edx,-0xc(%ebp) # char* b=argv[0];
mov 0xc(%ebp),%eax #
add $0x4,%eax #
mov (%eax),%edx #
mov %edx,-0x10(%ebp)# char* c=argv[1];
mov %ebp,%esp # EPILOG
pop %ebp # EPILOG
ret # EPILOG



Gereksiz satırları çıkardım. Öncelikle %esp 0x18-sekizli küçültülerek 4 adet 4-sekizlilik değişkenimize yer ayrılıyor yığıtta. Kalan 2-sekizli uzunluğundaki alanın ne amaçla ayrıldığı bu bölümün kapsamı dışında.

Daha sonra 0x8(%ebp) ile %eax yazmacı aracı olarak kullanılarak -0x4(%ebp)'e aktarılmakta. Buradan (%ebp+8)=argc ve (%ebp-4)=i olduğunu çıkarıyoruz. Aynı şekilde sonraki iki satırdan (%ebp+12)=argv ve (%ebp-8)=a olmakta. Böyle devam ettiğimizde yığıt yapısını aşağıdaki şekilde çıkarabiliriz:

Yığıtın durumunu bu bilgiler ışığında şematik olarak gösterelim:

Adres Erişim Değer

============= ÜST ADRES
N ebp+12 | argv | - Parametreler
|-----------|
N-04 ebp+8 | argc |
|-----------|
N-08 ebp+4 | Ret1 | - Dönüş adresi
|-----------|
N-12 ebp | %ebp | <--- [%ebp]
|-----------|
N-16 ebp-4 | int i | - Yerel değişkenler
|-----------|
N-20 ebp-8 | int a |
|-----------|
N-24 ebp-12 | int b |
|-----------|
N-28 ebp-16 | int c |
|-----------|
N-32 ebp-20 | (özel) |
|-----------|
N-36 ebp-24 | (özel) | <--- [%esp] (bulunduğu en alt adres)
|-----------|
N-40 ebp-28 | |
=============



Görüldüğü gibi yığıta erişimler hep %ebp'nin kullanılmasıyla yapılıyor. %ebp'ye göre negatif adresler yerel değişkenleri, pozitif adresler ise parametreleri gösteriyor. (özel) olan alanları unutun. Fakat iki satırın bu özel ajanlara ayrıldığına dikkat edin. Bir ayrıntıya dikkat çekmek için iki değişkeni azaltalım:

// Ornek2b.c
// ~~~~~~~~
int main(int argc, char* argv[])
{
int i=argc;
char** a=argv;

return 0;
}



Bu kodu gdb ile inceleyerek Ornek2 ile karşılaştıralım:

push %ebp # PROLOG
mov %esp,%ebp # PROLOG
sub $0x18,%esp # Yerel değişkenler
mov 0x8(%ebp),%eax #
mov %eax,-0x4(%ebp) # int i=argc;
mov 0xc(%ebp),%eax #
mov %eax,-0x8(%ebp) # char** a=argv;
mov %ebp,%esp # EPILOG
pop %ebp # EPILOG
ret # EPILOG



Yığıtın bu durumdaki yapısını çizelim:

Adres Erişim Değer

============= ÜST ADRES
N ebp+12 | argv | - Parametreler
|-----------|
N-04 ebp+8 | argc |
|-----------|
N-08 ebp+4 | Ret1 | - Dönüş adresi
|-----------|
N-12 ebp | %ebp | <--- [%ebp]
|-----------|
N-16 ebp-4 | int i | - Yerel değişkenler
|-----------|
N-20 ebp-8 | int a |
|-----------|
N-24 ebp-12 | --- |
|-----------|
N-28 ebp-16 | --- |
|-----------|
N-32 ebp-20 | (özel) |
|-----------|
N-36 ebp-24 | (özel) | <--- [%esp] (bulunduğu en alt adres)
|-----------|
N-40 ebp-28 | |
=============



Olmasını beklediğimiz gibi diğer iki değişken yığıttan atılıyor fakat dikkatli bakarsanız yığıtta yine de 0x18-sekizli uzunluğunda yer ayrıldığını göreceksiniz. Bu yeni Linux dağıtımlarında karşılaşabileceğiniz bir durumdur. Yeni gcc versiyonları yığıtta yer ayırırken en altta 8-sekizli uzunluğunda daha önce bahsettiğimiz özel alanlar dışında hep 16-sekizlinin katları şekinde yer ayırmaktadır. Yerel değişkenlerimiz 16-sekizliği geçtiği anda bu sefer 32-sekizlik bir alan açılmaktadır. Yani yığıtın uzunluğu yalnızca 0x18, 0x28, 0x38, 0x48 şeklinde olabilmektedir.


Basit "Olmayan" Yığıt Yapısı


Her şey açık sanırım. Şimdi de daha zorlayıcı bir örnek seçelim kendimize. İçiçe fonksiyonlar kullanıldığında neler olmaktadır? Ornek3 bu tür bir durumu örneklemektedir:

// Ornek3.c
// ~~~~~~~~
void fonk_ic_ic(f3)
{
char* d;

return;
}

int fonk_ic(int f1,int f2)
{
int c;
int d;

fonk_ic_ic(10,20);

return 31337;
}

int main(int argc, char* argv[])
{
int a;
char* Buff1="ABCDE";
char* Buff2="abcdefghi";
char b[13];

return(fonk_ic(100,200));
}



Arabellekler, yerel değişkenler, dönüş değerleri, kısaca bir C program yapısında olabilecek her şeyi yerleştirdik. (Buradan sonra her şey açık olmayabilir. Paniğe kapılmamak lazım. Gidip bir kahve hazırlamak iyi gelir Wink ).

$ gcc -o Ornek3 Ornek3.c
$ gdb Ornek3
GNU gdb...
(gdb) disass main
Dump of assembler code for function main:
0x80483fc : push %ebp
0x80483fd <main+1 />: mov %esp,%ebp
0x80483ff <main+3 />: sub $0x28,%esp
0x8048402 <main+6 />: movl $0x8048494,0xfffffff8(%ebp)
0x8048409 <main+13 />: movl $0x804849a,0xfffffff4(%ebp)
0x8048410 <main+20 />: add $0xfffffff8,%esp
0x8048413 <main+23 />: push $0xc8
0x8048418 <main+28 />: push $0x64
0x804841a <main+30 />: call 0x80483dc
0x804841f <main+35 />: add $0x10,%esp
0x8048422 <main+38 />: mov %eax,%edx
0x8048424 <main+40 />: mov %edx,%eax
0x8048426 <main+42 />: jmp 0x8048428 <main+44 />
0x8048428 <main+44 />: mov %ebp,%esp
0x804842a <main+46 />: pop %ebp
0x804842b <main+47 />: ret
0x804842c <main+48 />: lea 0x0(%esi,1),%esi
End of assembler dump.
(gdb)



main+6 ve main+13'deki satırlar gibi bazı komutlardaki adresler çok gizemli görünebilir. Büyüyü bozmak için gdb'nin çeşitli komutlarından yararlanabiliriz. Fakat en kolayı gcc'nin -S opsiyonunu kullanmak. Bu opsiyon derleme sonucu çevirici-dili çıktısı sağlamakta:

$ gcc -S -o Ornek3.S Ornek3.c
$ cat Ornek3.S
.file "Ornek3.c"
.version "01.01"
gcc2_compiled.:
.text
.align 4
.globl fonk_ic_ic
.type fonk_ic_ic,@function
fonk_ic_ic:
pushl %ebp
movl %esp,%ebp
subl $24,%esp
jmp .L2
.p2align 4,,7
.L2:
movl %ebp,%esp
popl %ebp
ret
.Lfe1:
.size fonk_ic_ic,.Lfe1-fonk_ic_ic
.align 4
.globl fonk_ic
.type fonk_ic,@function
fonk_ic:
pushl %ebp
movl %esp,%ebp
subl $24,%esp
addl $-8,%esp
pushl $20
pushl $10
call fonk_ic_ic
addl $16,%esp
movl $31337,%eax
jmp .L3
.p2align 4,,7
.L3:
movl %ebp,%esp
popl %ebp
ret
.Lfe2:
.size fonk_ic,.Lfe2-fonk_ic
.section .rodata
.LC0:
.string "ABCDE"
.LC1:
.string "abcdefghi"
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp,%ebp
subl $40,%esp
movl $.LC0,-8(%ebp)
movl $.LC1,-12(%ebp)
addl $-8,%esp
pushl $200
pushl $100
call fonk_ic
addl $16,%esp
movl %eax,%edx
movl %edx,%eax
jmp .L4
.p2align 4,,7
.L4:
movl %ebp,%esp
popl %ebp
ret
.Lfe3:
.size main,.Lfe3-main
.ident "GCC: (GNU) 2.95.3 20010315 (release)"



Bu çıktı gözünüzü hiç korkutmasın, birazdan oldukça aşina hale gelecek. Sadece "main" için değil tüm program için çıktı aldığımız için sonuç bu kadar uzun oldu. Şimdilik yalnızca "main:" kısmına dikkat edin. Sonunda iki-nokta-üst-üste olan ve soldan başlayan sözcükler yalnızca etiketlerdir. Yukarıdaki "main:" kodunu "disass main" çıktısı ile karşılaştırırsanız, gizemli 0x8048494 v.b. adreslerinin daha anlamlı etiketlere dönüştürüldüğünü görürsünüz. Aynı zamanda negatif değerler de normal görünmektedir.

$.LC0 ve $.LC1 karakter dizileri gibi statik veriler VERİ alanında bulunurlar. "main" kodunda PROLOG'dan sonra 40 (0x28) sekizlik yer ayrılmaktadır yığıtta. Şimdi biraz matematik yapalım ve bu 40 sayısının nerden geldiğini inceleyelim.

Öncelikle daha önce bahsettiğimiz özel alanlar için 8-sekizli derleyici tarafından yığıtın en altında ayrılıyor. Kaldı 32-sekizli.

"int a" değişkeni için 4-sekizli ayrıldığından kaldı 28-sekizli.

"char* Buff1="ABCDE"; char* Buff2="abcdefghi";" satırlarının karşılığı olan ilk-değer atamaları VERİ alanından yapılmakta. Burada dikkat etmemiz gereken konu karakter dizileri için yığıtta ne kadarlık yer kullanıldığıdır. Eğer "char * x = "xxxx";" şeklinde ilk-değer ataması yapıyorsak sabit değer kullanıyoruzdur ve bu değer derleyici tarafından VERİ alanından ayrılır. Dolayısıyla yığıtta yalnızca bu statik dizinin adresi tutulur. Buff1 ve Buff2 için bu durumda yığıtta yalnızca iki adres değeri dolayısı ile 8-sekizli ayrılacaktır. Kaldı 28-8=20-sekizli.

"char b[13];" şeklinde "[]" dizi göstergeleri ile tanımlanan ve uzunluğu belirtilen karakter dizileri ise doğrudan yığıtta tutulmaktadır. Hemen hatırlayalım ki belleğe erişim yalnızca 4-sekizlinin katları şeklinde olabilir. Dolayısıyla 13 karakter ve bir de sonlandırıcı NULL (0x00) karakteri ile 14-karakter yığıtta 14-sekizli değil, 16-sekizli kaplayacaktır. Kaldı 20-16=4-sekizli.

Bu 4-sekizli ise boş durmaktadır ve fazladan ayrılmıştır! Çünkü daha önce de belirttiğimiz gibi yeni gcc'ler önce 0x18-sekizli ayırmakta ve ardından bu yetmediğinde 0x10-sekizlik artışlar halinde yer ayırmaktadır. Bizim yerel değişkenlerimiz toplam 4+4+4+16=28-sekizli == 0x1c > 0x18 olduğundan fazladan bir 0x10 eklenerek 0x28'lik yer ayrılmaktadır. [ 0x28 - 0x1c = 0x0c ]. Bu 0x0c'nin 0x08'lik kısmı özel alan, kalan 0x0c-0x08=0x04'lük kısmı da başta bulduğumuz ve boş olarak bırakılan yığıt alanıdır.

Tamam 40 sayısının nedenini anladık. Kodu inceleyin. Daha sonra $.LC0 ve $.LC1 etiketleriyle tanımlanan sabit karakter dizilerimiz ilgili yerel değişkenlere aktarılıyor ki bunun kodu çok açık. Daha sonra da "fonk_ic(100,200)" çağrısı başlatılıyor. Fonksiyon çağrılırken öncelikle parametreler yığıta ilk önce en sağdaki olmak üzere yazılıyor. Daha sonra "call fonk_ic" kodu ile işlemci fonksiyonun dönüş adresini otomatik olarak yığıta ekliyor ve fonksiyonun kodunu çalıştırmaya başlıyor.

Bu arada atladığımız "addl -8(%esp)" komutunu inceleyelim. Neden doğrudan "PUSH"lara başlamayıp önce yığıtı 8-sekizli daha azaltıyoruz? Bunun cevabı basit: Yeni gcc versiyonlarında fonksiyon çağrılarında da yerel değişlenlerde olduğu gibi, yığıt 16-sekizlinin katları şeklinde ayrılmaktadır. Ayrıca parametreler bu 16-sekizlik alanın altından başlayarak yerleştirilmektedir. Böylece bizim 8-sekizli yer tutan 2 adet "int" parametremizin 16-sekizlik alanın alt 8-sekizlik kısmında yer alırken, üst 8-sekizlik kısmı bu kod ile boş bırakılmaktadır. Fonksiyon dönüşünde ise "addl +16,%esp" komutu ile parametreler için ayırdığımız 16-sekizlik alanı bertaraf etmekteyiz.

Sanırım şu anda kod inceleme mantığını kapmış durumdasınızdır. [Yani umudum o yönde]. Fonksiyonların kendi içlerindeki yığıt kullanımları main'in kullanımı ile temel olarak aynı olduğundan diğer kodları ayrıntılı olarak açıklamıyorum. Şimdi asıl önemli olan yığıt yapısının çizilmesi. Yığıtın en çok dolu olduğu halini bir çizimle gösterelim:

Adres Erişim Değer

(Gerçek)(main) (f_ic) (f_ic_ic)
============= ÜST ADRES
N ebp+12 | argv |
|-----------|
N-04 ebp+8 | argc |
|-----------|
N-08 ebp+4 | Ret1 |
|-----------|
N-12 ebp | ebp [xxxx]|
|-----------|
N-16 ebp-4 | int a |
|-----------|
N-20 ebp-8 | Buff1@ |
|-----------|
N-24 ebp-12 | Buff2@ |
|-----------|
N-28 ebp-16 | b[12] |
|-----------|
N-32 ebp-20 | b[8]-b[11]|
|-----------|
N-36 ebp-24 | b[4]-b[7] |
|-----------|
N-40 ebp-28 | b[0]-b[3] |
|-----------|
N-44 ebp-32 | --- |
|-----------|
N-48 ebp-36 | (özel) |
|-----------|
N-52 ebp-40 | (özel) |
|-----------| --------
N-56 ebp-44 | --- |
|-----------|
N-60 ebp-48 | --- |
|-----------|
N-64 ebp-52 ebp+12 | 200 |
|-----------|
N-68 ebp-56 ebp+8 | 100 |
|-----------|
N-72 ebp+4 | Ret2 |
|-----------|
N-76 ebp | ebp [N-12]|
|-----------|
N-80 ebp-4 | int c |
|-----------|
N-84 ebp-8 | int d |
|-----------|
N-88 ebp-12 | ---- |
|-----------|
N-92 ebp-16 | ---- |
|-----------|
N-96 ebp-20 | (özel) |
|-----------|
N-100 ebp-24 | (özel) |
|-----------| --------
N-104 ebp-28 | --- |
|-----------|
N-108 ebp-32 | --- |
|-----------|
N-112 ebp-36 ebp+12 | 20 |
|-----------|
N-116 ebp-40 ebp+8 | 10 |
|-----------|
N-120 ebp+4 | Ret3 |
|-----------|
N-124 ebp | ebp [N-76]|
|-----------|
N-128 ebp-4 | d |
|-----------|
N-132 ebp-8 | --- |
|-----------|
N-136 ebp-12 | --- |
|-----------|
N-140 ebp-16 | --- |
|-----------|
N-144 ebp-20 | (özel) |
|-----------|
N-148 ebp-24 | (özel) |
============= ALT ADRES



Yukarıdaki yığıt yapısını iyice anlamadan BOFE'lere giriş yapmak dahi mümkün olmayacağından bir kaç dakikanızı ayırıp yığıtın inceliklerini keşfe çıkın. Bu yapıyı çevirici çıktısı ile karşılaştırın.

Hemen yığıtın her bir düzey fonksiyon için farklı değerlere sahip %ebp yazmaçlarıyla adreslendiğini farkedebilirsiniz. Adreslenen bu farklı düzeylere "frame" ya da çerçeve adı verilir. Yukarıda "main" "f_ic" ve "f_ic_ic" olarak gösterilen üç farklı çerçeve için ebp adresleri yazılmış durumdadır. Fonksiyonların yerel değişkenlere erişmek için negatif, parametrelere erişmek içinse pozitif ebp adreslemesi kullanması gerektiğine dikkat edin.

BOFE dizimizin birinci bölümü burada bitiyor. İkinci bölümde arabellek taşırma kavramı ve kabuk-kod mantığına giriş yapacağız. 31337 kalın.

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

Taglar: advanced coding / buffer overflow exploit -1


Yorumlar


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