Testy aplikacji na Androida: analiza i zmiana sposobu działania aplikacji przez użycie frameworka Frida

29 czerwca 2017, 18:19 | Teksty | komentarze 3
: zin o bezpieczeństwie - pobierz w pdf/epub/mobi.

Wstęp

Dwa lata temu napisałem tekst na Sekuraku, w którym opisałem w jaki sposób można podejść do analizy aplikacji mobilnych na Androida – krótko poruszyłem tam temat dekompilacji plików .apk i pokazałem jak zmienić działanie funkcji, która w jakiś sposób „przeszkadza” w przeprowadzeniu testów takiej aplikacji (w realnych przykładach może to być aplikacja sprawdzająca, czy urządzenie jest zrootowane). Polegało to na disasemblacji pliku .apk do formatu plików .smali, a następnie do ich ponownej asemblacji do .apk. Następnie wymagane było ponowne zainstalowanie aplikacji na telefonie i cieszenie się z jej nowych funkcji ;)

Takie rozwiązanie niestety jest dość pracochłonne – wymaga ręcznej podmiany istniejących kodów .smali, jak również ponownej instalacji aplikacji. Życie było łatwiejsze, gdyby można było wykonać taką samą operację na żywej aplikacji, w żaden sposób nie zmieniając jej oryginalnego kodu. I tutaj z pomocą przychodzi: Frida.

Czym jest Frida

Frida, jak mówi strona internetowa tego projektu, to world-class dynamic instrumentation framework. Mówiąc po polsku: framework, który pozwoli nam wstrzyknąć nasz własny kod w działający proces (może być to proces na Androidzie, ale również wspierane są: iOS, Windows, Linux czy macOS), a następnie na kontrolę tego procesu z poziomu kodu javascriptowego. Jeśli nie jest to jasne, to przykład kilka akapitów niżej powinien rozwiać wszystkie wątpliwości

O ile kod, który będziemy wstrzykiwali w inny proces będzie napisany w JavaScripcie, o tyle sama obsługa Fridy może odbywać się w innych językach programowania, np. w Pythonie. Stąd najprostszym sposobem na instalację Fridy jest skorzystanie z polecenia:

Dzięki temu zainstalowane zostaną narzędzia frida, frida-kill, frida-ps, frida-discover, frida-ls-devices i frida-trace. Żeby Frida mogła jednak wstrzykiwać się w procesy na urządzeniu mobilnym, musimy na nim również uruchomić odpowiednią binarkę z serwerem. Binarki znajdziemy na githubie projektu, powinniśmy szukać plików, których nazwa zaczyna się od: frida-server. W przypadku mojego urządzenia, poprawnym plikiem będzie: frida-server-10.1.1-android-arm. Plik musimy rozpakować i wrzucić na telefon androidowy, a następnie uruchomić go na telefonie z uprawnieniami roota. Plik najlepiej skopiować narzędziem adb:

Serwer Fridy nie wypisuje żadnych komunikatów po uruchomieniu; jeśli wszystko jest OK, to „po prostu działa”.

Przed startem

Dla celów prezentacji Fridy, użyję tej samej aplikacji androidowej co w swoim poprzednim tekście. Dla przypomnienia: aplikacja próbuje pobrać plik ze ścieżki https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt i wyświetlić go. W domyślnej konfiguracji jednak na pewno się to nie uda ze względu na sposób implementacji sprawdzania certyfikatów. Dla większej przejrzystości tekstu, poniżej przypominam zdekompilowany kod obu klas pojawiających się w aplikacji pl.sekurak.ssltest:

Jeśli nie pamiętacie jak zdekompilować plik APK to postaci Javy to jeszcze raz odsyłam do poprzedniego tekstu.

Listing 1. Klasa pl.sekurak.ssltest.MainActivity:

Listing 2. Klasa pl.sekurak.ssltest.a

Kod, który szczególnie będzie nas interesował zawiera się w liniach 13-21 listingu 2. W tym miejscu sprawdzana jest poprawność certyfikatu w metodzie checkServerTrusted. Specyfika działania tej metody jest taka, że jeżeli certyfikat jest poprawny to metoda nie zwraca nic; jeśli natomiast certyfikat jest niepoprawny, zwracany jest wyjątek.

Podany powyżej kod metody checkServerTrusted jest tylko przykładem. Nie jest poprawnym sposobem sprawdzania poprawności certyfikatów SSL/TLS i nigdy nie należy podobnego kodu umieszczać w swojej aplikacji. O poprawnym podejściu do tematu sprawdzania certyfikatów można przeczytać w dokumentacji Androida.

Docelowo będziemy chcieli zmienić działanie metody tak, by nigdy nie rzucała wyjątku.

Testujemy Fridę

Wracamy więc do Fridy. Zakładamy, że mamy już uruchomioną Fridę na urządzeniu mobilnym. Pierwszą rzeczą, która nas interesuje to listing procesów. Wykorzystamy do tego polecenie frida-ps. Przekażemy do niej parametr -U oznaczający, że Frida spróbuje się komunikować z urządzeniem podłączonym przez USB

Dowiadujemy się dzięki temu, że PID procesu, w który będziemy się wstrzykiwać to 9717. Spróbujmy zatem wstrzyknąć się w proces poleceniem:

Dzięki parametrowi –enable-jit nasz kod javascriptowy będzie działał zdecydowanie szybciej (choć może być bardziej pamięciożerny).

Po poprawnym wykonaniu polecenia, powinniśmy zobaczyć obrazek podobny jak na Rys 1.

Rys 1. Uruchomienie polecenia "frida"

Rys 1. Uruchomienie polecenia „frida”

W konsoli możemy teraz wpisywać kod JS, który zostanie wykonany w kontekście aplikacji androidowej.

Co istotne, gdy działamy na Androidzie, powinniśmy zawsze nasz kod umieścić wewnątrz wywołania Java.perform – dzięki temu Frida upewnia nas, że kod zostaje wykonany w kontekście maszyny wirtualnej Javy.

Wylistowanie istniejących klas

Spróbujmy więc w pierwszej kolejności wypisać wszystkie klasy, które istnieją w działającej aplikacji. W Fridzie mamy do tego metodę Java.enumerateLoadedClasses, do której argumentem jest obiekt JS z dwoma polami:

  • onMatch – callback wykonywany w momencie znalezienia jakiejś klasy,
  • onComplete – callback wykonywany w momencie zakończenia operacji.

Wypiszmy sobie zatem wszystkie klasy istniejące w programie poniższym kodem:

Po uruchomieniu kodu, dostaniemy gigantyczną listę wszystkich klas (Rys 2.)

Rys 2. Wylistowanie klas w aplikacji

Rys 2. Wylistowanie klas w aplikacji

Wywoływanie metod na istniejących instancjach klas

Z poziomu Fridy możemy też wywoływać metody na istniejących klasach w aplikacji. Funkcja Java.choose() szuka żyjących instancji klasy, którą przekażemy jako argument. Podobnie jak enumerateLoadedClasses, jako drugi argument przyjmuje obiekt z definicją callbacków onMatch in onComplete.

Spróbujmy zatem poszukać żywej instancji klasy pl.sekurak.ssltest.a (to klasa z listingu 2) – jeśli takowa się znajdzie po prostu wypiszmy odpowiednią informację w konsoli. Użyjemy następującego kodu:

Na rys 3. możemy zobaczyć wynik wykonywania kodu.

Rys 3. Szukamy żywej instancji klasy

Rys 3. Szukamy żywej instancji klasy

Zapis „pl.sekurak.ssltest.a@236c1ee2” – pojawiający się na screenie – jest zapewne dobrze znany wszystkim programistom Javy. Jest to wynik standardowego wywołania metody toString() na obiekcie klasy. Wniosek: klasa pl.sekurak.ssltest.a rzeczywiście w pamięci istnieje!

Zatem teraz wywołajmy jakąś metodę na tej klasie. Na przykład po prostu checkServerTrusted przekazując jako argumenty dwa nulle:

Wynik widzimy na Rys 4.

Rys 4. Wywołanie metody checkServerTrusted na klasie

Rys 4. Wywołanie metody checkServerTrusted na klasie

W wyniku wykonania kodu wystąpił wyjątek – NullPointerException. Jest on jak najbardziej spodziewany, przypomnijmy sobie kod metody checkServerTrusted:

W trzeciej linii następuje odwołanie do zerowego indeksu tablicy paramArrayOfX509Certificate. Wcześniej nie ma jednak sprawdzenia czy ten obiekt nie jest nullem. Stąd naturalnie: NullPointerException, jako że w wywołaniu funkcji przekazaliśmy nulla.

Podmiana działania metody

Pokazaliśmy, że potrafimy wywołać metodę checkServerTrusted, ale teraz chcielibyśmy jeszcze podmienić jej działanie: powinna nie rzucać wyjątków, tylko zawsze natychmiast kończyć swoje działanie.

Żeby to wykonać, musimy najpierw odwołać się do klasy, w której jest zdefiniowana ta metoda – a następnie podmienić jej właściwość implementation na takie działanie jakie jest przez nas oczekiwane. Aby odwołać się do klasy samej w sobie używamy funkcji Java.use:

Kod oczywiście zadziała, jednak w naszej aplikacji mamy problem – bowiem ona próbuje pobrać treść pliku przez HTTPS tuż po uruchomieniu, więc podmiana działania metody w trakcie działania procesu jest już niewystarczająca.

W związku z tym musimy sami uruchomić nowy proces na telefonie i tak szybko jak tylko to możliwe – podmienić działanie metody. W tym celu użyjemy kodu napisanego w Pythonie według poniższego szablonu:

W zmiennej CODE definiujemy ten sam kod JS, który wcześniej wklejaliśmy w narzędziu Frida. Natomiast w dalszej części programu mamy odpowiednie przygotowanie środowiska po to, by podpiąć Fridę do procesu natychmiast po jego uruchomieniu.

Na rys 5. widzimy widok z wykonywania aplikacji bez Fridy, a na Rys 6 – z Fridą.

Rys 5. Uruchomienie testowej aplikacji bez Fridy

Rys 5. Uruchomienie testowej aplikacji bez Fridy

Rys 6. Uruchomienie testowej aplikacji z Fridą

Rys 6. Uruchomienie testowej aplikacji z Fridą

Próba podmiany metody zakończyła się sukcesem – w jej efekcie aplikacja nie sprawdza w żaden sposób poprawności certyfikatu SSL.

Użycie Fridy do logowania wywoływanych metod

Ostatnim ciekawym przypadkiem użycia Frida, który pokażę w tym artykule, jest logowanie wywoływanych metod – tj. z jakimi argumentami dana metoda została wywoływana. Jest to bardzo przydatne przy testach aplikacji mobilnych, gdy na podstawie analizy kodu trudno czasem powiedzieć, co dokładnie jest celem danej funkcji, zaś na podstawie analizy sposobu jej działania (argumenty i wartość zwracana) można to stwierdzić w okamgnieniu.

W klasie MainActivity do ustawiania tekstu widocznego w aplikacji mobilnej (jak np. „Wow, you made it!”) jest używana metoda setText:

Spróbujmy zatem przechwycić wszystkie wywołania metody setText w klasie TextView. Szybkie googlowanie pozwoli nam stwierdzić, że pełna nazwa klasy to: android.widget.TextView, a metoda setText jest przeładowana i może przyjmować różne kombinacje argumentów. Nie zastanawiajmy się nad tym, które dokładnie chcemy przechwycić – przechwyćmy wszystkie ;-)

Jeśli metoda Javowa jest przeładowana, to możemy wszystkie sposoby jej wywołania uzyskać przez dostęp do obiektu class.method.overloads. Dlatego w poniższym kodzie odwołam się do wszystkich metod typu setText, wypiszę na konsoli jakie argumenty zostały przekazane i wywołam oryginalną metodę:

Efekt widoczny na rys. 7.

Rys 7. Przechwycenie wywołań setText

Rys 7. Przechwycenie wywołań setText

Podsumowanie

Frida to bardzo rozbudowany framework, który może znacząco pomóc w analizie zachowania aplikacji mobilnych, dzięki możliwości przeprowadzenia poniższych operacji:

  • Analiza klas żyjących w pamięci,
  • Wywoływanie dowolnych metod na klasach żyjących w pamięci,
  • Przechwytywanie i podmiana działania metod,
  • Logowanie argumentów przekazywanych do wywoływanych metod.

Frida oferuje o wiele szersze możliwości niż opisane zostały w tym tekście; zainteresowanym polecam zerknąć do dokumentacji.

Michał Bentkowski, pentester   w Securitum

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. john

    swietny tekst! chociaz Firide znam i uzywam, super narzedzie do mobilnych aplikacji.

    Odpowiedz
  2. Piotr

    Wypadałoby napisać, że weyfikacja certyfikatu w podanym kodzie to tylko nic nie znaczący warunek, bo jeszcze ktoś coś takiego zaimplementuje i będzie dym…

    Czytajcie kod jaki wrzucacie, macie całkiem spory zasięg, więc i odpowiedzialność wieksza.

    Odpowiedz
    • Michał Bentkowski

      Chyba zakładałem w ciemno, że nikt nigdy nie spróbuje takiego kodu użyć w swojej aplikacji.

      Ale uwaga słuszna, dodałem już stosowną informację.

      Odpowiedz

Odpowiedz