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, welcher spezifische Verschleierungstyp verwendet wird.
Der Code auf der linken Seite ist eine Dekompilierung einer verschleierten App, und der Code auf der rechten Seite wurde entschleiert.
Das Projekt besteht aus drei Teilen: Smalivm, Simplify und der Demo-App.
if
oder switch
-Bedingung mit einem unbekannten Wert dazu, dass beide Zweige verwendet werden. 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
Für die Erstellung 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 Submodule jederzeit mit:
git submodule update --init --recursive
Um dann ein einzelnes Glas zu erstellen, das alle Abhängigkeiten enthält:
./gradlew fatjar
Die Simplify-JAR-Datei befindet sich in der Datei simplify/build/libs/
. Sie können die Funktionsweise testen, indem Sie die bereitgestellte verschleierte Beispiel-App vereinfachen. Gehen Sie folgendermaßen vor, um es auszuführen (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 entschleiert wird, schauen Sie sich die README-Datei von Obfuscated App an.
Wenn Simplify fehlschlägt, probieren Sie diese Empfehlungen in der Reihenfolge aus:
-it
nur auf einige wenige Methoden oder Klassen.--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 der Build unter Windows fehlschlägt und der Build mit einem Fehler wie dem folgenden fehlschlägt:
tools.jar konnte nicht gefunden werden. Bitte überprüfen Sie, ob C:ProgrammeJavajre1.8.0_151 eine gültige JDK-Installation enthält.
Dies bedeutet, dass Gradle keinen richtigen JDK-Pfad finden kann. Stellen Sie sicher, dass das JDK installiert ist, setzen Sie die Umgebungsvariable JAVA_HOME
auf Ihren JDK-Pfad und stellen Sie sicher, dass Sie die Eingabeaufforderung, die Sie zum Erstellen verwenden, schließen und erneut öffnen.
Seien Sie nicht schüchtern. Ich denke, dass virtuelle Ausführung und Entschleierung faszinierende Probleme sind. Jeder, der Interesse hat, ist automatisch cool und Beiträge sind willkommen, auch wenn es nur darum geht, einen Tippfehler zu beheben. Stellen Sie gerne Fragen in den Issues und reichen Sie Pull-Requests ein.
Bitte fügen Sie einen Link zum APK oder DEX und den vollständigen Befehl hinzu, den Sie verwenden. Dies macht es viel einfacher, Ihr Problem zu reproduzieren (und damit zu beheben ).
Wenn Sie das Beispiel nicht weitergeben 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, beispielsweise eine Zeichenfolge, eine Zahl oder einen booleschen Wert, ersetzt diese Optimierung diese Operation durch die Konstante. Zum 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 Zeichenfolge entschlüsselt und in v0
abgelegt. Da Strings „konstantisierbar“ sind, kann das move-result v0
durch einen 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 das Entfernen des Codes das Verhalten der App möglicherweise nicht ändern kann. Der offensichtlichste Fall ist, wenn der Code nicht erreichbar ist, z. B. if (false) { // dead }
). Wenn Code erreichbar ist, kann er als tot betrachtet werden, wenn er keinen Einfluss auf einen Zustand außerhalb der Methode hat, also keine Nebenwirkungen hat. Beispielsweise darf der Code keinen Einfluss auf den Rückgabewert der Methode haben, keine Klassenvariablen ändern oder E/A-Vorgänge ausführen. Dies ist in der statischen Analyse schwer zu bestimmen. Zum Glück muss Smalivm nicht clever sein. Es führt einfach dummerweise alles aus, was es kann, und geht davon aus, dass es Nebenwirkungen gibt, wenn es nicht sicher sein kann. Betrachten Sie das Beispiel aus Constant Propagation:
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 wirkt sich die invoke-static
nicht mehr auf den Rückgabewert der Methode aus. Gehen wir davon aus, dass sie keine ungewöhnlichen Aktionen wie das Schreiben von Bytes in das Dateisystem oder einen Netzwerk-Socket ausführt, sodass keine Nebenwirkungen auftreten. Es kann einfach entfernt werden.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Schließlich weist der erste const-string
einem Register einen Wert zu, dieser Wert wird jedoch nie verwendet, dh die Zuweisung ist ungültig. Es kann auch entfernt werden.
const-string v0, "Tell me of your homeworld, Usul."
Huzzah!
Eine große Herausforderung bei der statischen Analyse von Java ist die Reflexion. Es ist einfach nicht möglich, die Argumente für Reflexionsmethoden zu erkennen, ohne eine sorgfältige Datenflussanalyse durchzuführen. Es gibt clevere und clevere Möglichkeiten, dies zu tun, aber smalivm erledigt dies, indem es einfach den Code ausführt. Wenn ein reflektierter Methodenaufruf gefunden wird, wie zum Beispiel:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Es kann die Werte von v0
, v1
und v2
kennen. Wenn die Werte sicher sind, kann der Aufruf von Method.invoke()
durch einen tatsächlichen, nicht reflektierten Methodenaufruf ersetzt werden. Das Gleiche gilt für reflektierte Feld- und Klassensuchen.
Für alles, was nicht eindeutig in eine bestimmte Kategorie passt, gibt es Gucklochoptimierungen. Dazu gehört das Entfernen nutzloser check-cast
Operationen, das Ersetzen von Ljava/lang/String;-><init>
-Aufrufen durch const-string
und so weiter.
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
move-result v0
return v0
.end method
Alles, was dies bewirkt, ist v0 = 1
.
.method public static test1 () I
.locals 2
new-instance v0 , L java/lang/Integer ;
const/4 v1 , 0x1
invoke-direct { v0 , v1 }, L java/lang/Integer ; -> <init> ( I ) V
invoke-virtual { v0 }, L java/lang/Integer ; -> intValue () I
const/4 v0 , 0x1
return v0
.end method
Das move-result v0
wird durch const/4 v0, 0x1
ersetzt. Dies liegt daran, dass es für intValue()I
nur einen möglichen Rückgabewert gibt und der Rückgabetyp zu einer Konstante gemacht werden kann. Die Argumente v0
und v1
sind eindeutig und ändern sich nicht. Das heißt, es besteht ein Wertekonsens für jeden möglichen Ausführungspfad bei intValue()I
. Andere Arten von Werten, die in Konstanten umgewandelt werden können:
const/4
, const/16
usw.const-string
const-class
.method public static test1 () I
.locals 2
const/4 v0 , 0x1
return v0
.end method
Da der obige Code const/4 v0, 0x1
keinen Einfluss auf den Status außerhalb der Methode hat (keine Nebenwirkungen), kann er entfernt werden, ohne dass sich das Verhalten ändert. Wenn es einen Methodenaufruf gab, der etwas in das Dateisystem oder Netzwerk schrieb, konnte dieser nicht entfernt werden, da er den Status außerhalb der Methode beeinflusst. Oder wenn test()I
ein veränderliches Argument verwendet, beispielsweise eine LinkedList
, könnten alle Anweisungen, die darauf zugegriffen haben, nicht als tot betrachtet werden.
Weitere Beispiele für toten Code:
if (false) { dead_code(); }
Dieses Tool ist unter einer Doppellizenz erhältlich: einer kommerziellen, die für Closed-Source-Projekte geeignet ist, und einer GPL-Lizenz, die in Open-Source-Software verwendet werden kann.
Abhängig von Ihren Bedürfnissen müssen Sie eine davon auswählen und deren Richtlinien befolgen. Einzelheiten zu den Richtlinien und Vereinbarungen für jeden Lizenztyp finden Sie in den Dateien LICENSE.COMMERCIAL und LICENSE.GPL.