Tutorials

Android Hacking Kurs: Teil 1 – Dekompilieren & Source Code

This post is also available in: English

Im folgenden Tutorial schauen wir uns an, wie wir beliebige Android-Apps dekompilieren und so an den Source Code, also den Klartext-Quellcode der Anwendung kommen.

An einem konkreten Beispiel zeige ich außerdem, wie wir durch das Dekompilieren an geheime Informationen kommen können. Dafür steht eine von mir geschriebene Demo-App zum Download bereit. Als Bonus hab ich außerdem den Source Code der App auf GitHub hochgeladen. Im weiteren Verlauf sollte aber auch klar werden, dass wir den originalen Source Code eigentlich gar nicht brauchen.

Aber erstmal von vorne.

Warum dekompilieren?

Eine App wieder in den Source Code umwandeln – warum sollten wir sowas überhaupt wollen? Am besten erklärt sich das an der Demo-App, die ich für dieses Tutorial geschrieben habe:

Die App ist recht simpel gehalten und im Grunde nicht weiter als ein kleiner Tresor. Um den Tresor zu öffnen und an den Inhalt zu gelangen, muss die richtige Passwortkombination eingegeben werden. Klar könnten man jetzt anfangen blind los zuraten, aber wenn das Passwort nur etwas komplexer ist, dann sitzen wir in ein paar Jahren noch vor verschlossener Tür.

Aber: Um sagen zu können, ob das eingegebene Passwort richtig oder falsch ist, muss die Anwendung ja irgendwie wissen, wie das korrekte Passwort überhaupt lautet. Wenn wir es also schaffen, an den Source Code der Anwendung zu gelangen, dann kommen wir mit etwas Glück vielleicht auch an das Passwort für den Tresor.

Die App ist von mir zwar jetzt extra für dieses Tutorial geschrieben worden, das Beispiel ist aber nicht sonderlich weit weg von der Realität. Selbst in den Anwendungen großer Unternehmen finden sich im Source Code immer wieder vergleichbar sensible Informationen – sei es das Passwort eines Admin-Accounts, API-Tokens oder vielleicht sogar die Zugangsdaten zur angebundenen Datenbank. Außerdem können wir anhand des Source Codes die Funktionsweise der App viel genauer nachvollziehen und so leichter Schwachstellen darin finden.

Was ist eine Android-App eigentlich?

Android-Anwendungen werden als APK-Datei, im Normalfall direkt über Googles Play Store verteilt. APK steht dabei für Android Package und ist im Grunde nichts anderes als ein ZIP-Archiv mit geänderter Dateiendung.

Wenn wir also beispielsweise unsre Demo-Anwendung hernehmen und mit einem beliebigen ZIP-Tool entpacken, können wir uns theoretisch den gesamten Inhalt der App anschauen:

unzip -d vault-unzip vault.apk
Dateiinhalt der Android-App
Inhalt der entpackten APK-Datei

Das Problem an der Sache: das bringt uns alles noch nicht wirklich viel weiter.

Wenn wir uns beispielsweise die AndroidManifest.xml-Datei anschauen, sehen wir nur unleserlichen Binärcode. Die App ist zwar entpackt, aber schließlich immer noch kompiliert.

Kompilierte AndroidManifest.xml
Kompilierter Dateiinhalt der AndroidManifest.xml

Die classes.dex-Datei beinhalten den eigentlichen Code der Anwendung, aktuell ebenfalls noch als kompilierte Binärdatei. DEX steht hierbei für Dalvik Executable. Das Android-Betriebssystem erzeugt beim Start der App eine eigene virtualisierte Dalvik-Umgebung und führt die DEX-Datei dann darin aus.

Wenn wir es also schaffen, die classes.dex-Datei für uns lesbar zu machen, haben wir gute Chancen, an das Passwort zu kommen.

Disassembling vs. Decompiling

Dabei müssen wir jetzt auf der einen Seite zwischen Disassembling und auf der anderen Seite Decompiling unterscheiden.

Disassembling

Beim Disassembling wird der Bytecode in eine immer noch maschinennahe Sprache umgewandelt, die für uns aber lesbar ist. Bei Android-Apps sieht das so aus, dass die DEX-Datei in sogenannten Smali-Code umwandeln.

Den Smali-Code können wir dann mehr oder weniger gut analysieren oder aber auch beliebig manipulieren. Anschließend können wir die App wieder zusammenbauen und auf unsrem Smartphone ausführen. Dadurch können wir zum Beispiel gesperrte Funktionen einer App freischalten oder in einem Spiel an unbegrenzt viele Leben kommen. Wie wir sowas genau machen, zeig ich euch im zweiten Teil der Android-Hacking-Reihe.

Decompiling

Dekompilieren wandelt den Bytecode, im Gegensatz zum Disassembling, in eine höhere Programmiersprache um. Ziel ist es, den Kompilierungsvorgang umzukehren und den Source Code möglichst originalgetreu wiederherzustellen. Die Qualität des wiederhergestellten Codes hängt dabei stark von der Qualität des eingesetzten Decompilers ab.

Android-Anwendungen können auf zwei unterschiedliche Methoden dekompiliert werden. Bei der verbreitetsten Vorgehensweise wird die App in zwei getrennten Schritten dekompiliert. Warum dieses Vorgehen, trotz der Beliebtheit, zahlreiche Nachteile hat, wird sich im weiteren Verlauf noch sehr gut zeigen.

Methode 1: Dex → Jar → Java

Android Hacking: Dex to Jar to Java

In der ersten Variante wird der Dalvik-Binärcode der DEX-Datei zuerst in Java-Binärcode umgewandelt. Dabei fällt am Ende eine JAR-Datei heraus, d. h. ein kompiliertes Java-Archiv. Anschließend nutzen wir einen beliebigen Java-Decompiler, um aus der JAR-Datei wieder lesbare Java-Dateien zu machen. Diese Methode ist besonders beliebt, weil Java schon verhältnismäßige alt ist und entsprechend viele gute Decompiler dafür existieren.

Um die DEX-Datei in eine JAR-Datei umzuwandeln, verwenden wir dex2jar. Das Tool ist komplett Open Source und steht für alle Plattformen kostenlos zur Verfügung.

d2j-dex2jar vault.apk

Anschließend öffnen wir das JAR-Archiv mit dem Java-Decompiler JD-GUI. Dieser steht ebenfalls als Open-Source-Software kostenlos zur Verfügung.

Wir sehen jetzt, dass in der JAR-Datei insgesamt 4 Root-Packages enthalten sind. Die obersten drei Packages sind für uns erstmal uninteressant. Dort sind vor allem Hilfsbibliotheken enthalten, die uns egal sein können.

Im Package digital.basto.vault finden wir die wirklich interessanten Klassen.

Wir haben zum einen die BuildConfig mit einigen Meta-Informationen zu Anwendung, das UI-Package mit den Klassen, die das Benutzerinterface steuern, und das Data-Package, welches die Steuerung der Datenstruktur übernimmt.

Nach kurzer Suche entdecken wir die Klasse VaultDataSource. Dort findet sich relativ am Anfang der Parameter vaultCombination mit dem String Subscr1be!. Das klingt doch nach einem Wert, den wir mal in der App ausprobieren könnten.

Wir geben jetzt als Subscr1be! als Passwort in der App ein, bestätigen mit einem Klick auf „Unlock“ und…

…es funktioniert. Durch das Dekompilieren und Analysieren des Source Codes konnten wir den Tresor knacken und können nun auf das unermessliche Vermögen von 1337 Euro und 42 Cent zugreifen!

Noch ein kleiner Tipp für etwas komplexere Anwendungen:
Über den Menüpunkt „File → Save all Sources“ ist es in JD-GUI auch möglich, alle Klassen auf einmal zu dekompilieren und den gesamten Source Code zu exportieren. Anschließend kann der exportierten Source Code wieder in einem beliebigen Editor geöffnet werden. Dadurch lassen sich die Dateien besser durchsuchen oder Kommentare hinzufügen. Außerdem können den einzelnen Variablen und Methoden verständlichere Namen geben werden. Letzteres ist besonders hilfreich, um Code in einer Fremdsprache oder obfuskierten Code verständlicher zu machen.

Jetzt schauen wir uns an, wie wir DEX-Dateien bzw. ganze APKs ohne Umwege direkt in Java-Code dekompilieren.

Methode 2: APK → JAVA

Android Hacking: APK to Java

In der moderneren Vorgehensweise wird die APK-Datei direkt in die entsprechenden Java-Dateien umgewandelt. Der große Vorteil dabei ist, dass der Aufwand auf der einen Seite etwas geringer ist. Auf der anderen Seite gehen aber auch viel weniger Meta-Informationen auf der Strecke verloren. Dadurch lassen sich ebenso deutlich bessere Endergebnisse erzielen. Das größte Hindernis ist, dass es aktuell verhältnismäßig wenige Android-Decompiler auf der Markt gibt.

Um die App von Binärcode direkt in Java-Klassen dekompilieren zu können, verwenden wir den Android-Decompiler JADX. Mit JADX können wir die APK-Datei ohne Umwege öffnen und uns den Source Code dazu anzeigen lassen.

Dabei wird der größte Vorteil eines Android-Decompilers im Vergleich zu einem Java-Decompiler sichtbar. Wir haben neben dem Source Code aus der DEX-Datei auch das dekompilierte AndroidManifest, die Infos zum Zertifikat und aller Hand weiterer Metainformationen, die uns bei unsren Analysen eventuell weiterhelfen können.

Der direkte Vergleich: JD-GUI vs. JADX

Im direkten Vergleich zwischen dem generierten Source Code aus JADX und aus JD-GUI zeigt sich außerdem, dass JADX nicht nur das bessere Code-Highlighting hat, sondern auch deutlich bessere Ergebnisse liefert.

Originaler Code

Nachfolgend sehen wir zuerst den originalen Source Code der Datei, so wie ich ihn auch auf Github veröffentlicht habe. An die Methode unlock wird der String-Parameter password übergeben, der anschließend in einem If-Else-Vergleich überprüft wird.

Original

package digital.basto.vault.data;
import digital.basto.vault.data.model.VaultData;
import java.io.IOException;
import java.security.AccessControlException;

public class VaultDataSource {
    private String vaultCombination = "Subscr1be!";

    public Result<VaultData> unlock(String password) {
        try {
            if (vaultCombination.equals(password)) {
                VaultData unlockData = new VaultData(1337.42f);
                return new Result.Success<>(unlockData);
            } else {
                return new Result.Error(new AccessControlException("Wrong password!"));
            }
        } catch (Exception e) {
            return new Result.Error(new IOException("Error unlocking view!", e));
        }
    }

    public void lock() {
    }
}

Ergebnis von JD-GUI

Schauen wir uns jetzt zum Vergleich das Ergebnis von JD-GUI an, zeigt sich, dass zwar grundsätzlich die gleiche Logik herausgekommen ist, jedoch ist das Ergebnis deutlich komplexer zu analysieren. Zum einen ist die Methodenreihenfolgen umgedreht. Die lock-Methode wird vor der unlock-Methode aufgeführt. Außerdem heißt der Übergabeparameter statt password nur paramString. Das ist besonders verwirrend, da der gleiche Name auch noch einmal für die Exception verwendet wird. Um die Komplexität zu guter letzt noch ein Stückchen weiter zu erhöhen, verwendet JD-GUI statt des klassischen If-Else-Vergleichs nur die verkürzte Schreibweise <Bedingung> ? <True> : <False>.

JD-GUI

package digital.basto.vault.data;
import digital.basto.vault.data.model.VaultData;
import java.io.IOException;
import java.security.AccessControlException;

public class VaultDataSource {
  private String vaultCombination = "Subscr1be!";
  
  public void lock() {}
  
  public Result<VaultData> unlock(String paramString) {
    try {
      return this.vaultCombination.equals(paramString) 
          ? new Result.Success(new VaultData(1337.42F)) 
          : new Result.Error(new AccessControlException("Wrong password!"));
    } catch (Exception paramString) {
      return new Result.Error(new IOException("Error unlocking view!", paramString));
    } 
  }
}

Ergebnis von JADX

JADX hat seine Aufgabe im Gegensatz dazu vorbildlich erledigt. Die Methodenreihenfolge ist wie im Original und alle Parameter sind korrekt benannt. Lediglich der If-Else-Vergleich wurde auf einen If-Vergleich reduziert. Das schadet der Leserlichkeit jedoch nicht wirklich und manch einer würde diese Schreibweise sogar bevorzugen.

JADX

package digital.basto.vault.data;
import digital.basto.vault.data.Result.Error;
import digital.basto.vault.data.Result.Success;
import digital.basto.vault.data.model.VaultData;
import java.io.IOException;
import java.security.AccessControlException;

public class VaultDataSource {
    private String vaultCombination = "Subscr1be!";

    public Result<VaultData> unlock(String password) {
        try {
            if (this.vaultCombination.equals(password)) {
                return new Success(new VaultData(1337.42f));
            }
            return new Error(new AccessControlException("Wrong password!"));
        } catch (Exception e) {
            return new Error(new IOException("Error unlocking view!", e));
        }
    }

    public void lock() {
    }
}

Fazit

Damit haben wir den ersten und einen der wichtigsten Schritte in der Analyse von Android-Apps gelernt: Das effiziente Dekompilieren und die Wiederherstellung des Source Codes einer Anwendung.

Die Wahl der richtigen Werkzeuge ist bei der Suche nach Schwachstellen entscheidend. Die Auswahl an Android-Decompilern kann zwar nicht mit der Anzahl an Java-Decompilern mithalten, dennoch existiert mit JADX eine sehr gute und zudem auch kostenfreie Alternative. Neben zusätzlichen Meta-Informationen bietet JADX auch die Möglichkeit, den Code automatisch zu de-obfuskieren und liefert allgemein das bessere Resultat. Der direkte Vergleich der zwei Vorgehensweisen empfiehlt sich also alle mal.