Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Testy aplikacji na Androida: zmiana sposobu działania aplikacji przez rekompilację pliku APK
Wstęp
W ostatnich latach praca pentestera coraz częściej składa się też z testów aplikacji mobilnych. Czasem są one tylko cienką nakładką na strony internetowe, kiedy to testowanie ich niewiele różni się od webaplikacji, ale w wielu przypadkach są pełnoprawnymi, dużymi aplikacjami. W przeciwieństwie do typowych aplikacji webowych, gdzie logika zaimplementowana jest głównie po stronie serwera (choć to też przestaje być prawdę od pewnego czasu, gdyż coraz więcej dzieje się po stronie Javascriptu), w aplikacjach mobilnych mamy bezpośredni dostęp do plików wykonywalnych.
Czasem logika działania tychże aplikacji utrudnia bądź uniemożliwia testowanie. Przykładowo:
- Przy uruchomieniu aplikacja sprawdza, czy telefon jest zrootowany,
- Aplikacja definiuje własne metody kontroli ważności certyfikatów HTTPS, odmawiając współdziałania z proxy.
Oczywiście nie chcemy „odrootowywać” telefonu na potrzeby jednej aplikacji czy rezygnować z używania proxy, dlatego musimy znaleźć inny sposób, by poradzić sobie z problemem. Zasadniczo możemy zastosować jedno z trzech podejść:
- Przeanalizować sposób działania uciążliwej funkcji i odnaleźć w niej błąd (np. sprawdzanie czy telefon jest zrootowany może być niewystarczająco dobre),
- Zmienić w sposób dynamiczny sposób działania aplikacji (np. przez framework Xposed czy Cydia Substrate/Cycript w przypadku iOS),
- Zdekompilować aplikację, podmienić działanie złośliwej metody i zbudować ponownie plik .apk,
W tym artykule zajmiemy się wyłącznie trzecim sposobem. Do pozostałych (w szczególności do drugiego) prawdopodobnie jeszcze na łamach Sekuraka wrócimy.
Na potrzeby niniejszego artykułu przygotowałem prostą aplikację androidową, której jedynym celem jest próba pobrania pewnego pliku przez HTTPS. W domyślnej konfiguracji jednak się to nie uda (Rys 1.). W kolejnych rozdziałach prześledzimy działanie aplikacji (o której będę dalej pisał jako „testowa aplikacja”) i zmienimy jej kod w taki sposób, by wykonał się poprawnie.
Zanim zaczniemy…
W dalszej części tekstu zakładam, że do przeprowadzenia kolejnych operacji używamy dystrybucji linuksowej Kali, która zawiera w repozytorium (prawie) wszystkie potrzebne pakiety. Oczywiście opisane niżej kroki można wykonywać również na innych systemach, może jednak być wymagana ręczna instalacja narzędzi.
Krok 1. Pobranie pliku APK
Zacznijmy od tego, że musimy mieć plik APK, by mieć na czym przeprowadzać zmiany. W przypadku przeprowadzania testów penetracyjnych, zwykle podsyła je sam klient. Czasem jednak jesteśmy proszeni o instalację aplikacji bezpośrednio z Google Play. W takim wypadku, plik APK możemy pobrać na jeden z dwóch sposobów:
- Ustawiamy proxy w urządzeniu z Androidem, po czym pobieramy aplikację przez sklep Play. W jednej z odpowiedzi serwera na pewno znajdziemy pełną treść pliku APK,
- Jeśli mamy roota, plik APK można pobrać bezpośrednio z katalogu /data/app na urządzeniu.
My mamy konkretnego linka do aplikacji testowej, którą musimy najpierw pobrać:
test@mbkali:~/android$ wget http://training.securitum.com/apk/pl.sekurak.ssltest.apk test@mbkali:~/android$ md5sum pl.sekurak.ssltest.apk cf8cfdb6689237d3e7a988b943a5a5c6 pl.sekurak.ssltest.apk
Krok 2. Dekompilacja i poszukiwanie „winnej” metody
Problem z testową aplikacją jest taki, że próbuje pobrać plik przez HTTPS, jednak certyfikat SSL nie przechodzi walidacji. Możemy domniemywać, że gdzieś w kodzie znajduje się metoda odpowiedzialna za weryfikację poprawności certyfikatów. W tym kroku spróbujemy ją odnaleźć.
Potrzebować będziemy dwóch narzędzi: dex2jar oraz JD-GUI. W dystrybucji Kali ten pierwszy pakiet możemy ściągnąć bezpośrednio z repozytorium:
apt-get install dex2jar
JD-GUI też znajduje się w repozytorium, ale polecam jednak ściągnąć najnowszą wersję z oficjalnej strony aplikacji:
wget https://github.com/java-decompiler/jd-gui/raw/master/dist/jd-gui-1.0.0-RC2.jar
Plik classes.dex zawiera skompilowany kod aplikacji w postaci zrozumiałej dla wirtualnej maszyny Androida (zwanej Dalvik). Narzędzie dex2jar służy do zamiany pliku dex do pliku jar, który z kolei będzie można czytać przez dekompilator.
JD-GUI – to dekompilator plików .jar. Użyjemy go do wygodnego przeglądania kodu aplikacji androidowej.
Wyciągnijmy teraz plik classes.dex z pliku APK, a następnie zamieńmy go do postaci JAR-a.
test@mbkali:~/android$ unzip pl.sekurak.ssltest.apk classes.dex Archive: pl.sekurak.ssltest.apk inflating: classes.dex test@mbkali:~/android$ d2j-dex2jar classes.dex dex2jar classes.dex -> classes-dex2jar.jar test@mbkali:~/android$ ls classes.dex classes-dex2jar.jar jd-gui-1.0.0-RC2.jar pl.sekurak.ssltest.apk
Jak widzimy, utworzony został plik classes-dex2jar.jar. Odpalmy więc dekompilator Javy i zobaczmy, co jest w środku (Rys 2.).
java -jar jd-gui-1.0.0-RC2.jar
Z lewej strony widzimy listę klas, zaś po kliknięciu na jednej z nich, jej zdekompilowane źródło pojawia się w głównej części aplikacji. Jak widać, aplikacja testowa jest mało rozbudowana, bowiem składa się wyłącznie z dwóch klas. Oczywiście w rzeczywistych przypadkach najczęściej jest ich o wiele więcej i samo przeglądanie kodu w poszukiwaniu interesującej nas funkcji może zająć sporo czasu.
Szybko możemy znaleźć, że klasa pl.sekurak.ssltest.a zawiera metodę odpowiedzialną za sprawdzanie poprawności certyfikatów SSL.
package pl.sekurak.ssltest; import java.security.Principal; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; public class a implements X509TrustManager { public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) {} public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString) { if (paramArrayOfX509Certificate[0] == null) { throw new CertificateException(); } if (paramArrayOfX509Certificate[0].getIssuerDN().getName() != "CN=sekurakowy.pl,O=sekurak.pl,C=PL") { throw new CertificateException(); } } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }
Okazuje się, że aplikacja sprawdza, czy nazwa wystawcy certyfikatu to „CN=sekurakowy.pl,O=sekurak.pl,C=PL”. Oczywiście moglibyśmy na potrzeby testów wystawić certyfikat z taką nazwą. Spróbujemy jednak zmodyfikować aplikację w taki sposób, aby przepuszczała każdy certyfikat, niezależnie od jego parametrów.
Z tego kroku musimy zapamiętać, że metoda, której działanie będziemy chcieli zmienić, znajduje się w klasie pl.sekurak.ssltest.a i nazywa się checkServerTrusted. Logika działania metody jest taka, że jeśli certyfikat jest poprawny, metoda nie zwraca nic; zaś w przeciwnym wypadku rzucany jest wyjątek.
Krok 3. Rekompilacja i podpisanie pliku APK
Aby zacząć wprowadzać zmiany w kodzie pliku classes.dex, musimy najpierw skonwertować do formatu smali.
Zaczynamy od zainstalowania narzędzi smali:
apt-get install smali
Następnie konwertujemy plik classes.dex do postaci plików smali:
baksmali $PWD/classes.dex -o $PWD/smali
W wyniku wykonania powyższego polecenia, utworzony zostanie katalog smali składający się z wielu folderów oraz plików o rozszerzeniu .smali, odpowiadających klasom w pliku classes.dex (tym samym, które wcześniej oglądaliśmy z poziomu JD-GUI).
Pamiętamy, że chcieliśmy edytować klasę pl.sekurak.ssltest.a. Dlatego przyjrzymy się zawartości pliku smali/pl/sekurak/ssltest/a.smali:
.class public Lpl/sekurak/ssltest/a; .super Ljava/lang/Object; # interfaces .implements Ljavax/net/ssl/X509TrustManager; # direct methods .method public constructor <init>()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method public checkClientTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .registers 3 return-void .end method .method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .registers 5 const/4 v1, 0x0 aget-object v0, p1, v1 if-nez v0, :cond_b new-instance v0, Ljava/security/cert/CertificateException; invoke-direct {v0}, Ljava/security/cert/CertificateException;-><init>()V throw v0 :cond_b aget-object v0, p1, v1 invoke-virtual {v0}, Ljava/security/cert/X509Certificate;->getIssuerDN()Ljava/security/Principal; move-result-object v0 invoke-interface {v0}, Ljava/security/Principal;->getName()Ljava/lang/String; move-result-object v0 const-string v1, "CN=sekurakowy.pl,O=sekurak.pl,C=PL" if-eq v0, v1, :cond_1f new-instance v0, Ljava/security/cert/CertificateException; invoke-direct {v0}, Ljava/security/cert/CertificateException;-><init>()V throw v0 :cond_1f return-void .end method .method public getAcceptedIssuers()[Ljava/security/cert/X509Certificate; .registers 2 const/4 v0, 0x0 new-array v0, v0, [Ljava/security/cert/X509Certificate; return-object v0 .end method
Powyższy kod odpowiada dokładnie temu,co wkleiłem wcześniej w postaci źródła Javy; różnica polega na tym, że teraz mamy kod w postaci asemblera smali. Zmodyfikujmy kod metody checkServerTrusted w taki sposób, aby natychmiast kończyła swoje działanie. Powinna więc zawierać wyłącznie dyrektywę return-void.
.class public Lpl/sekurak/ssltest/a; .super Ljava/lang/Object; # interfaces .implements Ljavax/net/ssl/X509TrustManager; # direct methods .method public constructor <init>()V .registers 1 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method public checkClientTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .registers 3 return-void .end method .method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .registers 5 return-void .end method .method public getAcceptedIssuers()[Ljava/security/cert/X509Certificate; .registers 2 const/4 v0, 0x0 new-array v0, v0, [Ljava/security/cert/X509Certificate; return-object v0 .end method
Teraz musimy z powrotem zamienić pliki smali do classes.dex. Robimy to poleceniem smali:
smali $PWD/smali -o $PWD/classes.dex
A następnie władowujemy nowy classes.dex do pliku APK:
test@mbkali:~/android$ zip pl.sekurak.ssltest.apk classes.dex updating: classes.dex (deflated 58%)
Pliki APK są podpisane cyfrowo. Skoro zmieniliśmy zawartość pliku, podpis cyfrowy również się zmieni. Na szczęście w ramach dex2jar istnieje narzędzie, które pozwala szybko podpisać plik APK: d2j-apk-sign:
test@mbkali:~/android$ d2j-apk-sign pl.sekurak.ssltest.apk sign pl.sekurak.ssltest.apk -> pl.sekurak.ssltest-signed.apk
W wyniku wykonywania tego polecenia został utworzony nowy plik: pl.sekurak.ssltest-signed.apk. Pozostaje go tylko zainstalować na urządzeniu.
Krok 4. Instalacja nowego APK
Instalujemy teraz nowy plik apk na urządzeniu z Androidem. Musimy jednak wpierw odinstalować poprzednią wersję aplikacji testowej, ponieważ Android wykryje, że zmienił się podmiot podpisujący aplikację (poprzednio był to deweloper aplikacji, teraz d2j-apk-sign; Rys 3.).
Po odinstalowaniu i ponownej instalacji aplikacji, możemy wreszcie uruchomić ją ponownie. Powinniśmy na ekranie zobaczyć to samo co na Rys 4.
(wyświetlana jest zawartość pliku spod adresu https://raw.githubusercontent.com/securityMB/random-stuff/master/apk-file.txt)
Podsumowanie
W tekście przedstawiłem jak w prosty sposób można zmienić działanie aplikacji na Androida. Ogólnie proces rekompilacji aplikacji dla własnych potrzeb składa się z następujących elementów:
- Pobranie pliku .apk,
- Dekompilacja pliku .apk w celu odnalezienia metody, której działanie chcemy zmienić,
- Deasemblacja skompilowanego pliku classes.dex do plików smali,
- Modyfikacja wybranego pliku/wybranych plików smali,
- Ponowne skompilowanie pliku classes.dex i utworzenie nowego pliku .apk,
- Ponowny podpis pliku .apk.
Wkrótce na Sekuraku przedstawimy inne podejście do tego samego problemu – tj. jak zmienić działanie aplikacji bez modyfikacji oryginalnego pliku APK.
proste, gorzej jak apka jest obfuskowana i posiada z 500 klas ;)
Nee – jak wiesz czego szukać to znajdziesz to w 2 minuty (nazwy Javowych klas i metod nie mogą zostać zmienione)
ader, może podpowiesz na jakie klasy warto zwrócić uwagę? ;)
Warto zacząć właśnie od szukania metody checkServerTrusted lub po nazwie klasy SSLSocketFactory.
A jak exploita dodać do jakiejś aplikacj na przykład