Reverse engineering na przykładzie prostego crackme
Oprócz umiejętności programowania nieraz przydać się może także znajomość metod tzw. inżynierii odwrotnej, czyli analizy działania skompilowanych programów.
Posługują się nią m.in. osoby tworzące cracki czy generatory seriali pozwalające obejść zabezpieczenia licencyjne w oprogramowaniu. To też podstawowa umiejętność
pracowników firm antywirusowych, którzy analizują działanie wirusów. Przydać się też może w przypadku starego, niewspieranego oprogramowania, które nie może być
zastąpione innym. Dotyczy to np. sterowników starszych urządzeń. Jeśli nie są dostępne źródła aplikacji, trzeba spróbować zdekompilować jej plik wykonywalny
i ustalić w jaki sposób dany program działa. Podczas gdy w przypadku języków takich Java czy C# jest do dosyć proste, o tyle dekompilacja programu napisanego
w C czy C++ jest trudna i czasochłonna. Dzieje się tak ponieważ kompilator może dany kod źródłowy skompilować na wiele sposobów, zależnie np. od ustawionego stopnia
optymalizacji. Dekompilator musi więc bardzo często zgadywać i w wyniku dostaje się kod mało czytelny i trudny do analizy. Sytuacja na szczęście nie jest beznadziejna
i dysponując odpowiednią wiedzą i zaawansowanymi narzędziami (np.
IDA) można dosyć dokładnie
rozgryźć dany program.
Dla początkujących w dziedzinie reverse engineeringu oraz chcących sprawdzić swoje umiejętności, tworzone są tzw. crackme. Są to programy, w których trzeba
złamać hasło, napisać generator seriali lub w inny sposób wykazać, że rozgryzło się dane crackme. Często są rozprowadzane z podpowiedziami albo nawet gotowymi
rozwiązaniami. Jeśli więc nie uda się nam złamać danego crackme, możemy przeczytać jakich metod należało użyć i w ten sposób nauczyć się kolejnych metod.
W tym artykule chciałbym przedstawić łamanie
crackme stworzonego przez osobę o nicku Devoney. Jest to proste crackme, do którego analizy można wykorzystać nawet
najprostszy debugger/dekompilator. Ja użyłem
Immunity Debugger.
Naszym zadaniem jest podanie hasła.
Załadujmy więc crackme do debuggera. Wciskamy F9 aby przeskoczyć na początek kodu programu (adres 0x00401000).
Wciskamy F9 jeszcze raz, aby uruchomić program.
Lecz co się okazuje? Crackme posiada zabezpieczenie przed reversowaniem za pomocą debuggera. Na szczęście jest to proste crackme i możemy sobie z tym zabezpieczeniem
poradzić dosyć łatwo. Realizowane jest ono za pomocą wywołania funkcji
IsDebuggerPresent.
Normalnie funkcja ta zwraca zero, jednak gdy proces wywołujący działa pod kontrolą debuggera, funkcja zwraca wartość niezerową. Wartość zwracana jest standardowo,
przez rejestr EAX. Musimy więc sprawić, że w EAX będzie zero, nawet pod debuggerem. Przeglądając kod crackme (jest bardzo krótki, więc można ręcznie) widzimy, że funkcja IsDebuggerPresent jest wołana dwa razy:
raz z adresu 0x0040131A, a raz z 0x004013AF. W tych dwóch miejscach musimy więc zasymulować brak obecności debuggera. Aby program działał tak, jakby debuggera nie było,
trzeba wyzerować rejestr EAX. Można to zrobić np. instrukcją
XOR EAX, EAX
Ustawiamy więc kursor pod adresem 0x0040131A i wciskamy spację
Wpisujemy instrukcję, klikamy Assemble a następnie Cancel. OK, program mamy już spatchowany w jednym miejscu. Wykonujemy więc jeszcze taką samą operację dla adresu
0x004013AF.
Teraz można już przystąpić do analizy generowania hasła. Dla ułatwienia podzieliłem ją na punkty.
1. Program zaczyna się standardowo od adresu 0x00401000. Na początku skacze pod adres 0x00401017, gdzie znajduje się
kod wczytujący stringi i tworzący okienko.
2. Okienko tworzone jest funkcją DialogBoxIndirectParam. Ma ona parametr lpDialogFunc, który określa funkcję
przetwarzającą komunikaty okna. Tutaj jest ona pod adresem 00401308.
3. Procedura okna przetwarza przychodzące komunikaty. M.in. inicjalizuje wygląd okna w zależności od tego,
czy został wykryty debugger. Reaguje także na kliknięcie przycisku. Sygnalizowane jest to komunikatem WM_COMMAND.
Odpowiada mu liczba 0x111. Parametry są przekazywane do procedury przez stos. Dlatego możemy się do nich odwoływać
względem rejestru EBP:
EBP+8 - hWnd
EBP+C - uMsg
EBP+10 - wParam
EBP+14 - lParam
Aby więc sprawdzić czy został kliknięty przycisk, program porównuje zawartość adresu wskazywanego przez EBP+C z liczbą
0x111 (WM_COMMAND) a EBP+10 z 1 (identyfikator przycisku).
4. Jeśli nastąpiło kliknęcie przycisku, pobierany jest tekst z pola tekstowego i rozpoczyna się weryfikacja hasła.
Jest ona wykonywana poprzez wygenerowanie ciągu rozpoczynającego się pod adresem 0x0040304B. Ciąg ten będzie potem
porównany z wpisanym hasłem.
5. Liczba znaków hasła jest przez funkcję GetDlgItemTextA zwracana przez rejestr EAX. Program kopiuje tę wartość
do rejstru EBX, a następnie dodaje do niej 0x3C (60 dziesiętnie). W ten sposób dla hasła 5-znakowego otrzymamy liczbę
65, które odpowiada litera A. Dla 6-znakowego będzie to liczba 66 czyli litera B, itd. Wynikowa litera zapisywana
jest na pierwszej pozycji generowanego ciągu.
6. Wpisane przez użytkownika hasło trafiło pod adres 0x00403144. Widzimy, że program ładuje do EBX wartość spod adresu
0x00403146, a więc trzeci znak hasła. Dodaje do niego 0x31, czyli 49 dziesiętnie. Patrząc na tablicę ASCII widzimy,
że taka operacja może np. zamieniać kolejne cyfry na litery, czyli cyfrę 0 (w sensie znaku ASCII reprezentującego
zero, mającego wartość 0x30, dziesiętnie 48) na literę a (znak o kodzie 0x61, dziesiętnie 97).
7. Następnie ładowana do EBX jest wartość spod adresu 0x00403148, czyli piąty znak hasła. Od tej wartości odejmowane
jest 0x0A, czyli 10. Czyli trzeci znak hasła musi być o 10 mniejszy od piątego.
8. W kolejnym kroku do pierwszego znaku dodawane jest 0x20. Jeśli znakiem jest litera, to z tabeli ASCII widzimy, że
dodanie 0x20 powoduje zamianę wielkiej litery na małą.
9. Potem analogicznie od szóstego znaku odejmowane jest 0x22 aby otrzymać znak piąty.
10. Szósty znak otrzymywany jest przez odjęcie 5 od drugiego znaku hasła. Następuje więc tu pewna zmiana, podczas
gdy znaki 2-5 ciągu były generowane względem wpisanego hasła, znaki 6-8 generowane są na podstawie znaków ciągu.
11. Siódmy znak otrzymywany jest przez odjęcie 0x28 od znaku szóstego.
12. Przy ósmym znaku widzimy, że od znaku siódmego odejmowane jest pięć, jednak wynik tej operacji nie jest używany.
Do pamięci wpisywana jest po prostu liczba zero, która powoduje zakończenie ciągu.
13. Możemy więc generowanie hasła przedstawić w postaci następującej tabelki, gdzie X - wpisane hasło, Y - ciąg generowany
przez crackme i porównywany z hasłem. Jako że liczymy znaki, pozwoliłem sobie indeksować od 1.
Y[1] = len(X) + 0x3C, np. litera C dla siedmioznakowego hasła
Y[2] = X[3] + 0x31, np. a dla 0
Y[3] = X[5] - 0x0A
Y[4] = X[1] + 0x20, wielka litera na małą
Y[5] = X[6] - 0x22
Y[6] = Y[2] - 0x05
Y[7] = Y[6] - 0x28
Y[8] = 0
14. To w sumie tyle jeśli chodzi o sam kod crackme. Znając algorytm weryfikacji hasła spróbujmy wygenerować poprawne
hasło. Jak widać, hasło walidowane jest względem samego siebie, w tym jego długości. Crackme generuje ciąg o długości
siedmiu znaków, wpisywane hasło musi mieć więc tyle samo. Znając długość hasła znamy zatem jego pierwszą literę.
Będzie to C. Możemy to zapisać tak:
C______
Wiemy też, że czwarta litera hasła jest taka jak pierwsza, ale mała, mamy zatem:
C__c___
Następnie widzimy, że zależności zapętlają się, bowiem znak 2 zależy od 3, 3 od 5, 5 od 6, a 6 znów od 2. Możemy więc sobie wybrać jakiś znak początkowy i według niego obliczyć
pozostałe. Jest on ogólnie dowolny, jednak trzeba wziąć pod uwagę dwie rzeczy: znak musi dać się wpisać z klawiatury, odpadają więc znaki niedrukowalne. Żaden ze znaków nie może
też być znakiem zerowym (bajtem o wartości 0x00), bo będzie on oznaczał zakończenie ciągu. Przyjmijmy więc, że znakiem na pozycji 2 będzie a, i zobaczmy jakie otrzymamy pozostałe znaki.
Ca_c___
Mając znak 2 otrzymujemy znak 6. Znakiem 2 jest a, czyli bajt 0x61. Odejmując 5 otrzymujemy bajt 0x5C czyli znak \.
Ca_c_\_
Ze znaku 6 obliczamy znak 5, odejmując 0x22 od 0x5C dostajemy 0x3A, czyli :.
Ca_c:\_
Ze znaku 5 obliczamy znak 3 przez odjęcie 0x0A, otrzymując 0x30, czyli 0.
Ca0c:\_
Ze znaku 3 możemy obliczyć znak 2, ale jego wartość już sobie przyjęliśmy na początku. Możemy więc dokonać sprawdzenia, czy faktycznie różnią się one o 0x31. Faktycznie, taka
jest właśnie różnica pomiędzy 0x61 a 0x30. Zostaje nam ostatni, siódmy znak. Jest to znak (bajt) szósty, pomniejszony o 0x28. Znak 6 to \ czyli 0x5C, a więc znak 7 to 0x34 czyli 4.
Ca0c:\4
W ten oto sposób złamaliśmy hasło analizowanego crackme :)