
Machine virtuelle Android et désobfuscateur
Simplify exécute virtuellement une application pour comprendre son comportement, puis tente d'optimiser le code afin qu'il se comporte de manière identique mais soit plus facile à comprendre pour un humain. Chaque type d'optimisation est simple et générique, de sorte que le type spécifique d'obfuscation utilisé n'a pas d'importance.
Le code à gauche est une décompilation d'une application obfusquée, et le code à droite a été désobfusqué.
Le projet comporte trois parties : smalivm, simplify et l'application de démonstration.
if ou switch avec une valeur inconnue conduit à la prise des deux branches.usage : java -jar simplify.jar <input> [options]
désobfusque un exécutable Dalvik
-et,--exclude-types <pattern> Exclure les classes et méthodes qui incluent REGEX, ex: "com/android", appliqué après include-types
-h,--help Afficher ce message
-ie,--ignore-errors Ignorer les erreurs lors de l'exécution et de l'optimisation des méthodes. Cela peut conduire à un comportement inattendu.
--include-support Tenter d'exécuter et d'optimiser les classes dans les packages de la bibliothèque de support Android, par défaut : false
-it,--include-types <pattern> Limiter l'exécution aux classes et méthodes qui incluent REGEX, ex: ";->targetMethod\("
--max-address-visits <N> Abandonner l'exécution d'une méthode après avoir visité la même adresse N fois, limite les boucles, par défaut : 10000
--max-call-depth <N> Ne pas appeler de méthodes après avoir atteint une profondeur d'appel de N, limite la récursion et les longues chaînes de méthodes, par défaut : 50
--max-execution-time <N> Abandonner l'exécution d'une méthode après N secondes, par défaut : 300
--max-method-visits <N> Abandonner l'exécution d'une méthode après avoir exécuté N instructions dans cette méthode, par défaut : 1000000
--max-passes <N> Ne pas exécuter les optimiseurs sur une méthode plus de N fois, par défaut : 100
-o,--output <file> Écrire l'entrée simplifiée dans FICHIER
--output-api-level <LEVEL> Définir la compatibilité API DEX de sortie au NIVEAU, par défaut : 15
-q,--quiet Être silencieux
--remove-weak Supprimer le code même en cas d'effets secondaires faibles, par défaut : true
-v,--verbose <LEVEL> Définir la verbosité au NIVEAU, par défaut : 0
La construction nécessite l'installation du Java Development Kit 8 (JDK).
Comme ce projet contient des sous-modules pour les frameworks Android, clonez avec --recursive :
git clone --recursive https://github.com/CalebFenton/simplify.git
Ou mettez à jour les sous-modules à tout moment avec :
git submodule update --init --recursive
Ensuite, pour construire un jar unique contenant toutes les dépendances :
./gradlew fatjar
Le jar Simplify se trouvera dans simplify/build/libs/. Vous pouvez tester son fonctionnement en simplifiant l'exemple d'application obfusquée fourni. Voici comment l'exécuter (vous devrez peut-être modifier simplify.jar) :
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
Pour comprendre ce qui est désobfusqué, consultez le README de l'application obfusquée.
Si Simplify échoue, essayez ces recommandations, dans l'ordre :
-it.--max-address-visits, --max-call-depth et --max-method-visits.-v ou -v 2 et signalez le problème avec les logs et un hachage du DEX ou APK.Si la construction sous Windows échoue avec une erreur similaire à :
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Cela signifie que Gradle ne parvient pas à trouver un chemin JDK approprié. Assurez-vous que le JDK est installé, définissez la variable d'environnement JAVA_HOME sur votre chemin JDK, et assurez-vous de fermer et de rouvrir l'invite de commande que vous utilisez pour la construction.
Ne soyez pas timide. Je pense que l'exécution virtuelle et la désobfuscation sont des problèmes fascinants. Toute personne intéressée est automatiquement cool et les contributions sont les bienvenues, même s'il s'agit simplement de corriger une faute de frappe. N'hésitez pas à poser des questions dans les issues et à soumettre des pull requests.
Veuillez inclure un lien vers l'APK ou le DEX et la commande complète que vous utilisez. Cela facilite grandement la reproduction (et donc la correction) de votre problème.
Si vous ne pouvez pas partager l'échantillon, veuillez inclure le hachage du fichier (SHA1, SHA256, etc.).
Si une opération place une valeur d'un type qui peut être transformé en constante, comme une chaîne, un nombre ou un booléen, cette optimisation remplacera cette opération par la constante. Par exemple :
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
Dans cet exemple, une chaîne chiffrée est déchiffrée et placée dans v0. Comme les chaînes sont « constantisables », le move-result v0 peut être remplacé par un const-string :
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."
Le code est mort si sa suppression ne peut pas altérer le comportement de l'application. Le cas le plus évident est si le code est inaccessible, par exemple if (false) { // mort }). Si le code est accessible, il peut être considéré comme mort s'il n'affecte aucun état en dehors de la méthode, c'est-à-dire s'il n'a pas d'effet secondaire. Par exemple, le code peut ne pas affecter la valeur de retour de la méthode, modifier des variables de classe, ou effectuer des E/S. C'est difficile à déterminer en analyse statique. Heureusement, smalivm n'a pas besoin d'être intelligent. Il exécute bêtement tout ce qu'il peut et suppose qu'il y a des effets secondaires s'il n'en est pas sûr.
Considérez l'exemple de la Propagation de constantes :
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."
Dans ce code, le invoke-static n'affecte plus la valeur de retour de la méthode et supposons qu'il ne fait rien d'étrange comme écrire des octets dans le système de fichiers ou un socket réseau, donc il n'a pas d'effets secondaires. Il peut simplement être supprimé.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Enfin, le premier const-string assigne une valeur à un registre, mais cette valeur n'est jamais utilisée, c'est-à-dire que l'affectation est morte. Elle peut également être supprimée.
const-string v0, "Tell me of your homeworld, Usul."
Huzzah !
L'un des principaux défis de l'analyse statique de Java est la réflexion. Il n'est tout simplement pas possible de connaître les arguments des méthodes de réflexion sans effectuer une analyse minutieuse du flux de données. Il existe des moyens intelligents et astucieux de le faire, mais smalivm le fait en exécutant simplement le code. Lorsqu'il trouve une invocation de méthode réfléchie telle que :
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Il peut connaître les valeurs de v0, v1 et v2. S'il est sûr de ces valeurs, il peut remplacer l'appel à Method.invoke() par une invocation de méthode non réfléchie réelle. Il en va de même pour les recherches de champs et de classes réfléchies.
Pour tout ce qui ne rentre pas proprement dans une catégorie particulière, il y a les optimisations de type peephole. Cela inclut la suppression des opérations check-cast inutiles, le remplacement des appels Ljava/lang/String;-><init> par const-string, etc.
.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
Tout cela fait 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
Le move-result v0 est remplacé par const/4 v0, 0x1. Cela est dû au fait qu'il n'y a qu'une seule valeur de retour possible pour intValue()I et que le type de retour peut être transformé en constante. Les arguments v0 et v1 sont sans ambiguïté et ne changent pas. Autrement dit, il y a un consensus de valeurs pour chaque chemin d'exécution possible au niveau de intValue()I.
Autres types de valeurs pouvant être transformées en constantes :
const/4, const/16, etc.const-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
Parce que le code au-dessus de const/4 v0, 0x1 n'affecte pas l'état en dehors de la méthode (aucun effet secondaire), il peut être supprimé sans changer le comportement. S'il y avait un appel de méthode qui écrivait quelque chose dans le système de fichiers ou le réseau, il ne pourrait pas être supprimé car il affecte l'état en dehors de la méthode. Ou si test()I prenait un argument mutable, comme un LinkedList, toute instruction qui y accédait ne pourrait pas être considérée comme morte.
Autres exemples de code mort :
if (false) { dead_code(); }Cet outil est disponible sous une double licence : une licence commerciale adaptée aux projets à source fermée et une licence GPL pouvant être utilisée dans les logiciels open source.
Selon vos besoins, vous devez en choisir une et suivre ses politiques. Le détail des politiques et accords pour chaque type de licence est disponible dans les fichiers LICENSE.COMMERCIAL et LICENSE.GPL.