Yığın, son girenin ilk çıktığı (LIFO) bir doldur boşalt veri alanıdır. Yığın genel olarak aşağıdaki amaçlarla kullanılmaktadır.
Her fonksiyon çağrıldığında, yığın üzerinde o fonksiyona ait bilgilerin tutulduğu yeni bir alan ayrılır. Bu alana yığın çerçevesi (stack frame) denilmektedir. Yeni oluşturulan yığın çerçevesinin sınırlarına ait bilgiler esp ve ebp yazmaçlarında tutulmaktadır.
Not: esp yazmacı aktif yığın çerçevesinin üst noktasının adresini, ebp yazmacı ise yerel değişkenler için ayrılan alanın sonunu göstermektedir. esp yığın göstericisi (stack pointer), ebp ise çerçeve göstericisi (frame pointer) olarak isimlendirilmektedir.
Tipik bir yığın çerçevesi aşağıdaki gibidir.
Basit bir örnek üzerinden yığının durumunu inceleyelim.
#include <stdio.h>
void foo(int arg) {
int local = arg;
}
int main() {
foo(111);
return 0;
}
Örnek uygulamaya st1.c adını vererek aşağıdaki gibi derleyebilirsiniz.
gcc -ost1 st1.c -m32 --save-temps
Derleyicinin main ve foo fonksiyonları için aşağıdaki gibi sembolik makina kodları ürettiğini görmekteyiz.
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $111, (%esp)
call foo
movl $0, %eax
leave
ret
foo:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax
movl %eax, -4(%ebp)
leave
ret
Not: Sembolik makina kodlarındaki .cfi ile başlayan assembler direktifleri sadeleştirme amaçlı olarak atılmıştır.
Sembolik makina kodlarına yakından bakacak olursak, main fonksiyonunun yığının tepe noktasına 111 değerini geçirdiğini ve ardından foo fonksiyonunu çağırdığını görüyoruz.
movl $111, (%esp)
call foo
call makina komutu foo fonksiyonunu çağırmadan önce bir sonraki komutun adresini (eip yazmacındaki değeri) yığına geçirmektedir. Bu adres fonksiyonun geri dönüş değeri olarak kullanılmaktadır. foo çağrıldığında yığının durumu aşağıdaki gibidir.
foo fonksiyonunun, ilk olarak bir önceki çerçeve göstericisini sakladığını, ardından yeni çerçeveye ilişkin adresi ebp yazmacına yazdığını görüyoruz.
pushl %ebp
movl %esp, %ebp
Bu aşamada yığına tekrar bakalım.
Daha sonra yığında yerel değişkenler için 16 byte yer ayrıldığını görmekteyiz.
subl $16, %esp
Sonraki iki komutta, fonksiyona geçirilen argüman önce eax yazmacına oradan da yerel local değişkeni için ayrılan alana yazılmış.
movl 8(%ebp), %eax
movl %eax, -4(%ebp)
Bu aşamada yığına tekrar bakalım.
Sonraki leave makina komutu ile fonksiyonun başında yapılan işlemler geri alınmış. esp yazmacı ebp değerine çekilmiş, ardından eski çerçeve değeri yığından çekilerek ebp yazmacına yazılmış. leave makina komutunun eşdeğeri aşağıdaki gibidir.
movl %ebp, %esp
popl %ebp
Akış ret makina komutuna geldiğinde yığının görüntüsü aşağıdaki gibidir.
ret makina komutu, esp yazmacının gösterdiği bellek alanındaki adrese dallanmakta ve bu değeri yığından çekmektedir.
Şimdi bir de, bizi daha çok ilgilendiren bir durum olan, yerel bir tampon kullanımına ilişkin aşağıdaki örneği inceleyelim.
#include <stdio.h>
void foo() {
char buf[8] = {0};
}
int main() {
foo();
return 0;
}
Örnek uygulamaya st2.c adını vererek aşağıdaki gibi derleyebilirsiniz.
gcc -ost2 st2.c -m32 -fno-stack-protector --save-temps
Not: Derleyiciye geçirdiğimiz fno-stack-protector anahtarı, derleyicinin yığın taşmalarını (stack overflow) tespit edebilmek için fazladan kod yazmasını engellemektedir. Bu özellikten daha sonra bahsedeceğiz.
foo için derleyici aşağıdaki gibi bir kod üretmektedir.
foo:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $0, -8(%ebp)
movl $0, -4(%ebp)
leave
ret
Sembolik makina kodlarına baktığımızda yerel değişkenler için yığında 16 byte yer ayrıldığını ve bu alanın sonunda tampon için ayrılan 8 byte uzunluğundaki alanın 0 değeriyle ilklendirildiğini görmekteyiz.
subl $16, %esp
movl $0, -8(%ebp)
movl $0, -4(%ebp)
Tampon saldırılarındaki hedef genel olarak tampon alanını ve sonrasındaki geri dönüş adresini bozarak, dışarıdan enjekte edilen veya proses adres alanında var olan bir kodu çalıştırmaktır.
Konunun başında tampon saldırılarının genel olarak iki şekilde yapıldığından bahsetmiştik. Şimdi bu durumları inceleyelim.