poniedziałek, 26 lipca 2010

Dekompilacja kodu w J2ME

Kiedyś ściągnąłem pewną aplikację (napisaną w J2ME) na mój telefon i po pewnym czasie użytkowania okazało się, że należy podać do niej kod aktywacyjny (za który oczywiście należy zapłacić). Zastanawiałem się, czy da się jakoś to ominąć i poniżej opiszę rezultaty mojej rozkminki, jak sobie z tym poradzić.

Jak wiadomo kod Javy jest kompilowany do bytecode'u, a ten uruchamiany na wirtualnej maszynie Javy (eng. Java Virtual Machine, JVM). Dzięki temu kompilujemy raz, a uruchamiamy wszędzie (tzn. tam gdzie jest Java Wirtual Machine). Ilustruje to poniższy obrazek:




Skoro kod nie jest kompilowany do natywnego kodu języka danego urządzenia, to możemy skorzystać z tak zwanej wstecznej inżynierii (ang. reverse engineering). Właściwie to każdy plik wykonywalny można zdekompilować, tylko w przypadku aplikacji pisanych nie w Javie, zazwyczaj po dekompilacji mamy odczynienia z kodem asemblerowym, a ten nie należy do przyjemnych w czytaniu. W przypadku Javy mamy trochę lepiej.

W sieci dostępnych jest kilka dekompilatorów kodu Javowego (DJ Java Decompiler  Java Decompiler  Mocha i inne). Ja posłużę się tym pierwszym. Na cele tego artykułu stworzyłem niewielką aplikację, aby pokazać jak skorzystać z możliwości wstecznej dekompilacji. Nie chciałem łamać jakiejś innej aplikacji, aby nie być posądzonym o ingerencje w nią, co pewnie się wiąże z łamaniem prawa. Z własną aplikacją mogę robić co chcę :P

Zacznijmy więc. Na początek należy zawsze uruchomić aplikację i zobaczyć czego szukamy. Jak nie chce nam sie wrzucać jej na telefon, a mamy zainstalowane Java ME platform SDK możemy odpalić ją na komputerze w ten sposób:

<Java ME platform SDK path>\bin\emulator.exe -Xdescriptor:<JAD file path>

Oczywiście należy podać odpowiednie ścieżki, gdzie mamy zainstalowane SDK i gdzie leży nasza aplikacja. W innych SDK (starszych wersjach, lub innych producentów) mogą być graficzne narzędzia umożliwiające odpalenie aplikacji J2ME na komputerze. Poniżej przedstawiam jak wygląda moja aplikacja odpalona na emulatorze:



Po lewej mamy ekran startowy (specjalnie dodałem kilka opcji, aby coś było), a po prawej ekran po wybraniu opcji "Activation". W ekranie tym należy wpisać odpowiedni kod aktywacyjny. To jest miejsce na którym nam zależy i które chcemy złamać.

Odpalamy DJ Java Decompiler. Następnie File -> Open jako typ pliku wybieramy: Java Archive Files(*.jar) i wskazujemy naszego JARa. Ukazuje nam się poniżej okno podobne do poniższego:


Wciskamy Ctrl + A aby zaznaczyć wszystkie klasy i klikamy Decompile. Wskazujemy miejsce docelowe i w pojawiającym sie komunikacie klikamy Yes i po chwili w wybranym folderze otrzymujemy wynik. Plik *.class jest plikiem otrzymanym bezpośrednio przez wypakowanie JARa (gdyz de facto to zwykłe archiwum), a w pliku *.jad mamy zdekompilowany kod. Ciekaw jestem czemu akurat takie rozszerzenie zostało wybrane przez twórców aplikacji, skoro jest ono używane w procesie instalacji aplikacji komórkowych? Można było przecież utworzyć pliki *.java.

No dobra zajrzyjmy do zdekompilowanego pliku. Kod wygląda czytelnie. Porównałem go KDiff3'em z oryginalnym kodem i po za modyfikacją importów, zmianą standardu kodowania, niewielkimi modyfikacjami i dodanymi kilkoma komentarzami kod sie zasadniczo nie różnił. Utworzyłem nowy projekt i wrzuciłem ten "odzyskany" kod i się poprawnie skompilował i poprawnie działa!

Przejdźmy do analizy kodu. Na początku ukazuje nam się konstruktor klasy (część linii pominięto i zachowano oryginalne formatowanie):




Na początku są tworzone obiekty typu Command i widzimy, że to co nas interesuje ("Activation") jest dodawane do List'y. Następnie dodawane jest okCommand i co najważniejsze wywołanie setCommandListener z przekazaniem obiektu this. Oznacza to, że nasza klasa implementuje CommandListener, do którego są kierowane zdarzenia z naszej formatki. Sprawdzamy powyżej:



Rzeczywiście. Przejdźmy więc do metody zdefiniowanej przez ten interfejs:



Na początku jest sprawdzenie czy obiekt command odpowiada naszej liście opcji. Jeśli tak, to sprawdzane jest czy była to komenda okCommand. Jeśli tak to pobierany jest index aktualnie zaznaczonej opcji i coś się dalej dzieje. Jako, że opcja "Activation" byla na 3ciej pozycji w menu (widać to na screenie), to patrzymy co się dzieje pod dwójką ;)



Wywoływana jest jakaś metoda i następnie na wyświetlaczu jest ustawiane do wyświetlenia activationForm. Dowiedzmy się więcej o tym obiekcie. Wróćmy do konstruktora:



Do activationForm dodawane są dwie kontrolki i znów CommandListener ustawiony na this. Wróćmy wiec do metody commandAction() i zjedźmy trochę niżej:



Tak ten warunek to miejsce gdzie trafia wykonanie naszej aplikacji, gdy wciśniemy Ok na activationForm. Zobaczmy co się dzieje poniżej:



Najpierw pobierany jest tekst z kontrolki, która jest odpowiedzialna za wprowadzenie kodu aktywacyjnego. Następnie sprawdzana jest długość pobranego tekstu i gdy jest ona różna od 12 to wywoływana jest metoda showBadActivationCodeMessage(). Sama nazwa juz sugeruje, że nie jest to nic dobrego. Jaki wniosek z tego? Nasz kod aktywacyjny musi mieć 12 znaków długości. Sprawdźmy co się dzieje gdy warunek ten jest spełniony:



Pobierane są kolejno 3ci 8my i 11sty znak z naszego kodu aktywacyjnego. I jeśli odpowiadają one odpowiednio literom 'A', 'B' i 'C' to odnosimy sukces.

Sprawdźmy więc. Odpalamy naszą aplikację na emulatorze (lub telefonie) i wpisujemy np.: xxxAxxxxBxxCx i prosze: Code OK. Złamaliśmy w ten sposób aplikację J2ME.

Jak widać można w prosty sposób podejrzeć kod obecny w aplikacjach J2ME. Jak sie jednak ustrzec przed tego typu ingerencją w oprogramowanie? Właściwie nie można w 100% się ustrzec przed tego typu atakami. Można jedynie utrudnić śmiałkom zadanie.

Najprostszym sposobem ochrony naszego kodu jest stosowanie tzw. obfluskacji (eng. Obfuscation). Polega ona na "upiększaniu kodu", w taki sposób, aby po dekompilacji ciężej było go odczytać. Jest kilka tego typu narzędzi na rynku. Jedno jest dołączone do Netbeansa. Jeśli mamy projekt J2ME utworzony w tym środowisku klikamy prawym przyciskiem myszy na nasz projekt i przechodzimy do Properties. Następnie klikamy na Obfluscating. Tutaj możemy juz sobie ustawić żądany poziom obfluskacji:



Klikamy OK i przebudowujemy nasz projekt. Teraz po wstecznej dekompilacji kod jest mniej czytelny, np.: pole options zmieniło nazwę na a_javax_microedition_lcdui_List_fld, a inne pola skróciły się do jednoliterowych nazw. Jest to trudniejsze w czytaniu, ale narzędzia pozwalające kolorować składnię trochę nam pomagają. Tutaj pokazuję przykład działania na prostym przykładzie i może efekt nie jest powalający. Przy dużych projektach narzędzia takie potrafią namieszać tak, że kod uzyskany za pomocą wstecznej inżynierii nie uda sie ponownie skompilować!!!

Dlaczego tak sie dzieje? Otóż to co jest dozwolone z poziomu kompilatora javy jest również dozwolone w bytecode'dzie. Jednak są pewne elementy bytecode'u, które nie są dozwolone z poziomu składni języka Java. Np. w bytecode'dzie mogą być metody o tej samej nazwie, przyjmujące te same argumenty a zwracające różną wartość! Odpowiednia metoda zostanie wywołana na podstawie typu do którego będzie rezultat przypisywany, lub jeszcze jakiś innych przesłanek. Analiza takiego kodu jest o wiele trudniejsza i bez dobrego refaktoringu nie da się obejść.

Na nasze szczęście (lub nie) klasa rozszerzająca MIDlet możne być poddana tylko częściowej obfluskacji, więc zawsze łatwo będzie nam znaleźć miejsce wejścia do programu:) Również nazwy metod dostarczanych przez javę (np. w klasie String) nie mogą być zmienione, co trochę ułatwia analizę takiego kodu.

Jak jeszcze można się bronić przed wsteczną dekompilacją? W J2ME nie ma mechanizmu ClassLoader'a, więc w ten sposób nie zabezpieczymy naszej aplikacji. Jedyne co po za obfluskacją przychodzi mi do głowy to własna implementacja odpowiednich mechanizmów zabezpieczających.

Poniżej zamieszczam jeszcze spakowany projekt, na podstawie którego prezentowałem zagadnienie. Będziecie mogli się sami pobawić i zobaczyć jak to działa.


PasswordNeed.zip (17KB)

Brak komentarzy:

Prześlij komentarz