
Android-Virtual-Maschine und Deobfuskator
Simplify führt eine App virtuell aus, um ihr Verhalten zu verstehen, und versucht dann, den Code so zu optimieren, dass er sich identisch verhält, aber für einen Menschen leichter zu verstehen ist. Jeder Optimierungstyp ist einfach und generisch, sodass es keine Rolle spielt, welche spezifische Art der Verschleierung verwendet wird.
Der Code auf der linken Seite ist eine Dekompilierung einer verschleierten App, und der Code auf der rechten Seite wurde entobfuskiert.
Das Projekt besteht aus drei Teilen: smalivm, simplify und der Demo-App.
if- oder switch-Bedingung mit unbekanntem Wert beide Zweige ausgeführt.usage: java -jar simplify.jar <input> [options]
deobfuscates a dalvik executable
-et,--exclude-types <pattern> Exclude classes and methods which include REGEX, eg: "com/android", applied after include-types
-h,--help Display this message
-ie,--ignore-errors Ignore errors while executing and optimizing methods. This may lead to unexpected behavior.
--include-support Attempt to execute and optimize classes in Android support library packages, default: false
-it,--include-types <pattern> Limit execution to classes and methods which include REGEX, eg: ";->targetMethod\("
--max-address-visits <N> Give up executing a method after visiting the same address N times, limits loops, default: 10000
--max-call-depth <N> Do not call methods after reaching a call depth of N, limits recursion and long method chains, default: 50
--max-execution-time <N> Give up executing a method after N seconds, default: 300
--max-method-visits <N> Give up executing a method after executing N instructions in that method, default: 1000000
--max-passes <N> Do not run optimizers on a method more than N times, default: 100
-o,--output <file> Output simplified input to FILE
--output-api-level <LEVEL> Set output DEX API compatibility to LEVEL, default: 15
-q,--quiet Be quiet
--remove-weak Remove code even if there are weak side effects, default: true
-v,--verbose <LEVEL> Set verbosity to LEVEL, default: 0
Zum Erstellen muss das Java Development Kit 8 (JDK) installiert sein.
Da dieses Projekt Submodule für Android-Frameworks enthält, klonen Sie entweder mit --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
Oder aktualisieren Sie die Submodule jederzeit mit:
git submodule update --init --recursive
Um dann ein einzelnes JAR zu erstellen, das alle Abhängigkeiten enthält:
./gradlew fatjar
Das Simplify-JAR befindet sich in simplify/build/libs/. Sie können testen, ob es funktioniert, indem Sie die bereitgestellte verschleierte Beispiel-App vereinfachen. So führen Sie es aus (möglicherweise müssen Sie simplify.jar ändern):
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
Um zu verstehen, was entobfuskiert wird, sehen Sie sich das README der verschleierten App an.
Wenn Simplify fehlschlägt, versuchen Sie diese Empfehlungen in der Reihenfolge:
-it.--max-address-visits, --max-call-depth und --max-method-visits.-v oder -v 2 und melden Sie das Problem mit den Protokollen und einem Hash des DEX oder APK.Wenn unter Windows erstellt wird und der Build mit einem Fehler ähnlich dem folgenden fehlschlägt:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Das bedeutet, dass Gradle keinen gültigen JDK-Pfad finden kann. Stellen Sie sicher, dass das JDK installiert ist, setzen Sie die Umgebungsvariable JAVA_HOME auf Ihren JDK-Pfad und schließen und öffnen Sie die Eingabeaufforderung, die Sie zum Erstellen verwenden, erneut.
Seien Sie nicht schüchtern. Ich halte virtuelle Ausführung und Entobfuskation für faszinierende Probleme. Jeder, der interessiert ist, ist automatisch cool, und Beiträge sind willkommen, selbst wenn es nur darum geht, einen Tippfehler zu korrigieren. Stellen Sie gerne Fragen in den Issues und reichen Sie Pull Requests ein.
Bitte fügen Sie einen Link zum APK oder DEX sowie den vollständigen Befehl hinzu, den Sie verwenden. Das erleichtert die Reproduktion (und damit die Behebung) Ihres Problems erheblich.
Wenn Sie das Beispiel nicht teilen können, fügen Sie bitte den Datei-Hash (SHA1, SHA256 usw.) hinzu.
Wenn eine Operation einen Wert eines Typs platziert, der in eine Konstante umgewandelt werden kann, z. B. eine Zeichenkette, Zahl oder Boolean, ersetzt diese Optimierung diese Operation durch die Konstante. Beispiel:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# Decrypts to: "Tell me of your homeworld, Usul."
move-result v0
In diesem Beispiel wird eine verschlüsselte Zeichenkette entschlüsselt und in v0 platziert. Da Zeichenketten „konstantisierbar“ sind, kann move-result v0 durch ein const-string ersetzt werden:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
Code ist tot, wenn seine Entfernung das Verhalten der App unmöglich verändern kann. Der offensichtlichste Fall ist, wenn der Code unerreichbar ist, z. B. if (false) { // dead }. Wenn Code erreichbar ist, kann er als tot betrachtet werden, wenn er keinen Zustand außerhalb der Methode beeinflusst, d. h. er hat keinen Nebeneffekt. Beispielsweise beeinflusst Code möglicherweise den Rückgabewert der Methode nicht, ändert keine Klassenvariablen und führt keine E/A durch. Dies ist bei der statischen Analyse schwer zu bestimmen. Glücklicherweise muss smalivm nicht clever sein. Es führt einfach alles aus, was es kann, und nimmt Nebeneffekte an, wenn es sich nicht sicher ist. Betrachten Sie das Beispiel aus der Konstantenpropagation:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
const-string v0, "Tell me of your homeworld, Usul."
In diesem Code beeinflusst invoke-static nicht mehr den Rückgabewert der Methode, und nehmen wir an, es macht nichts Seltsames wie das Schreiben von Bytes in das Dateisystem oder in einen Netzwerk-Socket, sodass es keine Nebeneffekte hat. Es kann einfach entfernt werden.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Schließlich weist das erste const-string einem Register einen Wert zu, aber dieser Wert wird nie verwendet, d. h. die Zuweisung ist tot. Es kann ebenfalls entfernt werden.
const-string v0, "Tell me of your homeworld, Usul."
Hurra!
Eine große Herausforderung bei der statischen Analyse von Java ist Reflection. Es ist einfach nicht möglich, die Argumente für Reflection-Methoden zu kennen, ohne eine sorgfältige Datenflussanalyse durchzuführen. Es gibt intelligente, clevere Möglichkeiten, dies zu tun, aber smalivm tut es, indem es den Code einfach ausführt. Wenn es einen reflektierten Methodenaufruf wie diesen findet:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Kann es die Werte von v0, v1 und v2 kennen. Wenn es sicher ist, was die Werte sind, kann es den Aufruf von Method.invoke() durch einen tatsächlichen, nicht reflektierten Methodenaufruf ersetzen. Gleiches gilt für reflektierte Feld- und Klassenlookups.
Für alles, was nicht sauber in eine bestimmte Kategorie passt, gibt es Peephole-Optimierungen. Dazu gehört das Entfernen nutzloser check-cast-Operationen, das Ersetzen von Ljava/lang/String;-><init>-Aufrufen durch const-string usw.
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I
move-result v0
return v0
.end method
Alles, was dies tut, ist v0 = 1.
.method public static test1()I
.locals 2
new-instance v0, Ljava/lang/Integer;
const/4 v1, 0x1
invoke-direct {v0, v1}, Ljava/lang/Integer;-><init>(I)V
invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I
const/4 v0, 0x1
return v0
.end method
move-result v0 wird durch const/4 v0, 0x1 ersetzt. Dies liegt daran, dass es nur einen möglichen Rückgabewert für intValue()I gibt und der Rückgabetyp konstant gemacht werden kann. Die Argumente v0 und v1 sind eindeutig und ändern sich nicht. Das heißt, es gibt einen Konsens der Werte für jeden möglichen Ausführungspfad bei intValue()I. Andere Werttypen, die in Konstanten umgewandelt werden können:
const/4, const/16 usw.const-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
Da der Code oberhalb von const/4 v0, 0x1 den Zustand außerhalb der Methode nicht beeinflusst (keine Nebeneffekte), kann er entfernt werden, ohne das Verhalten zu ändern. Wenn es einen Methodenaufruf gäbe, der etwas in das Dateisystem oder Netzwerk schreibt, könnte er nicht entfernt werden, da er den Zustand außerhalb der Methode beeinflusst. Oder wenn test()I ein veränderliches Argument wie LinkedList nähme, könnten alle Anweisungen, die darauf zugreifen, nicht als tot betrachtet werden.
Weitere Beispiele für toten Code:
if (false) { dead_code(); }Dieses Tool ist unter einer dualen Lizenz verfügbar: einer kommerziellen Lizenz, die für Closed-Source-Projekte geeignet ist, und einer GPL-Lizenz, die in Open-Source-Software verwendet werden kann.
Je nach Ihren Bedürfnissen müssen Sie eine davon auswählen und deren Richtlinien befolgen. Ein Detail der Richtlinien und Vereinbarungen für jeden Lizenztyp finden Sie in den Dateien LICENSE.COMMERCIAL und LICENSE.GPL.