RS232 book cover


Centrum Outlook Express

OE PowerTool 4.5.3

Twój adres IP to: 54.167.15.6
Przeglądarka: CCBot/2.0 (https://commoncrawl.org/faq/)


Ludwig von Mises Institute

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.

crackme

Załadujmy więc crackme do debuggera. Wciskamy F9 aby przeskoczyć na początek kodu programu (adres 0x00401000).

debugger

Wciskamy F9 jeszcze raz, aby uruchomić program.

crackme

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ę

debugger

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.

debugger

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.

debugger

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

crackme

W ten oto sposób złamaliśmy hasło analizowanego crackme :)