Linux Yazılım Güvenliği

Yığın (Stack)

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.

  • otomatik ömürlü yerel değişken tahsisatı
  • yazmaçların saklanması
  • fonksiyonların geri dönüş değerlerinin saklanması

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.