
Macchina virtuale Android e deoffuscatore
Simplify esegue virtualmente un'app per comprenderne il comportamento e poi cerca di ottimizzare il codice in modo che si comporti identicamente ma sia più facile da comprendere per un umano. Ogni tipo di ottimizzazione è semplice e generico, quindi non importa quale specifico tipo di offuscamento sia stato utilizzato.
Il codice a sinistra è una decompilazione di un'app offuscata, e il codice a destra è stato deoffuscato.
Ci sono tre parti nel progetto: smalivm, simplify e l'app demo.
if o switch con un valore sconosciuto porta a prendere entrambi i rami.usage: java -jar simplify.jar <input> [options]
deobfuscates a dalvik executable
-et,--exclude-types <pattern> Esclude classi e metodi che includono REGEX, ad es: "com/android", applicato dopo include-types
-h,--help Mostra questo messaggio
-ie,--ignore-errors Ignora errori durante l'esecuzione e l'ottimizzazione dei metodi. Ciò potrebbe portare a comportamenti imprevisti.
--include-support Tenta di eseguire e ottimizzare le classi nei pacchetti della libreria di supporto Android, default: false
-it,--include-types <pattern> Limita l'esecuzione a classi e metodi che includono REGEX, ad es: ";->targetMethod\("
--max-address-visits <N> Abbandona l'esecuzione di un metodo dopo aver visitato lo stesso indirizzo N volte, limita i cicli, default: 10000
--max-call-depth <N> Non chiamare metodi dopo aver raggiunto una profondità di chiamata di N, limita ricorsione e lunghe catene di metodi, default: 50
--max-execution-time <N> Abbandona l'esecuzione di un metodo dopo N secondi, default: 300
--max-method-visits <N> Abbandona l'esecuzione di un metodo dopo aver eseguito N istruzioni in quel metodo, default: 1000000
--max-passes <N> Non eseguire ottimizzatori su un metodo più di N volte, default: 100
-o,--output <file> Output semplificato su FILE
--output-api-level <LEVEL> Imposta la compatibilità API DEX di output al LIVELLO, default: 15
-q,--quiet Stai zitto
--remove-weak Rimuovi codice anche se ci sono effetti collaterali deboli, default: true
-v,--verbose <LEVEL> Imposta la verbosità al LIVELLO, default: 0
La costruzione richiede l'installazione del Java Development Kit 8 (JDK).
Poiché questo progetto contiene sottomoduli per i framework Android, clona con --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
Oppure aggiorna i sottomoduli in qualsiasi momento con:
git submodule update --init --recursive
Quindi, per costruire un unico jar che contiene tutte le dipendenze:
./gradlew fatjar
Il jar di Simplify sarà in simplify/build/libs/. Puoi testare che funzioni semplificando l'app di esempio offuscata fornita. Ecco come eseguirla (potresti dover cambiare simplify.jar):
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
Per capire cosa viene deoffuscato, consulta README di Obfuscated App.
Se Simplify fallisce, prova questi suggerimenti, in ordine:
-it.--max-address-visits, --max-call-depth e --max-method-visits.-v o -v 2 e segnala il problema con i log e un hash del DEX o APK.Se stai costruendo su Windows e la costruzione fallisce con un errore simile a:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Ciò significa che Gradle non riesce a trovare un percorso JDK corretto. Assicurati che JDK sia installato, imposta la variabile d'ambiente JAVA_HOME al percorso del tuo JDK e assicurati di chiudere e riaprire il prompt dei comandi che usi per costruire.
Non essere timido. Penso che l'esecuzione virtuale e il deoffuscamento siano problemi affascinanti. Chiunque sia interessato è automaticamente figo e i contributi sono benvenuti, anche solo per correggere un errore di battitura. Sentiti libero di fare domande negli issue e inviare pull request.
Includi un link all'APK o DEX e il comando completo che stai usando. Questo rende molto più facile riprodurre (e quindi risolvere) il tuo problema.
Se non puoi condividere il campione, per favore includi l'hash del file (SHA1, SHA256, ecc).
Se un op piazza un valore di un tipo che può essere trasformato in una costante come una stringa, numero o booleano, questa ottimizzazione sostituirà quell'op con la costante. Ad esempio:
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
invoke-static {v0}, Lmy/string/Decryptor;->decrypt(Ljava/lang/String;)Ljava/lang/String;
# Decifra in: "Tell me of your homeworld, Usul."
move-result v0
In questo esempio, una stringa crittografata viene decifrata e inserita in v0. Poiché le stringhe sono "costantizzabili", move-result v0 può essere sostituito con 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."
Il codice è morto se la sua rimozione non può alterare in alcun modo il comportamento dell'app. Il caso più ovvio è se il codice è irraggiungibile, ad es. if (false) { // morto }). Se il codice è raggiungibile, può essere considerato morto se non influisce su alcuno stato al di fuori del metodo, cioè non ha effetto collaterale. Ad esempio, il codice potrebbe non influenzare il valore di ritorno del metodo, alterare variabili di classe o eseguire I/O. Questo è difficile da determinare nell'analisi statica. Fortunatamente, smalivm non deve essere intelligente. Semplicemente esegue stupidamente tutto ciò che può e assume che ci siano effetti collaterali se non può esserne sicuro. Considera l'esempio dalla Propagazione delle Costanti:
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 questo codice, invoke-static non influisce più sul valore di ritorno del metodo e supponiamo che non faccia cose strane come scrivere byte nel filesystem o su un socket di rete, quindi non ha effetti collaterali. Può essere semplicemente rimosso.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Infine, il primo const-string assegna un valore a un registro, ma quel valore non viene mai utilizzato, cioè l'assegnazione è morta. Può anche essere rimosso.
const-string v0, "Tell me of your homeworld, Usul."
Evviva!
Una delle principali sfide nell'analisi statica di Java è la riflessione. Semplicemente non è possibile conoscere quali siano gli argomenti per i metodi di riflessione senza fare un'attenta analisi del flusso di dati. Ci sono modi intelligenti e furbi per farlo, ma smalivm lo fa semplicemente eseguendo il codice. Quando trova un'invocazione di metodo riflesso come:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Può conoscere i valori di v0, v1 e v2. Se è sicuro di quali siano i valori, può sostituire la chiamata a Method.invoke() con una reale invocazione di metodo non riflessa. Lo stesso vale per le ricerche riflesse di campi e classi.
Per tutto ciò che non si adatta perfettamente a una particolare categoria, ci sono le ottimizzazioni peephole. Ciò include la rimozione di op check-cast inutili, la sostituzione di chiamate Ljava/lang/String;-><init> con const-string, e così via.
.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
Tutto ciò fa è 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 viene sostituito con const/4 v0, 0x1. Questo perché c'è un solo possibile valore di ritorno per intValue()I e il tipo di ritorno può essere reso una costante. Gli argomenti v0 e v1 sono non ambigui e non cambiano. Cioè, c'è un consenso di valori per ogni possibile percorso di esecuzione in intValue()I. Altri tipi di valori che possono essere trasformati in costanti:
const/4, const/16, ecc.const-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
Poiché il codice sopra const/4 v0, 0x1 non influisce sullo stato al di fuori del metodo (nessun effetto collaterale), può essere rimosso senza cambiare comportamento. Se ci fosse stata una chiamata a un metodo che scriveva qualcosa sul filesystem o sulla rete, non potrebbe essere rimossa perché influisce sullo stato al di fuori del metodo. O se test()I avesse preso un argomento mutabile, come un LinkedList, qualsiasi istruzione che lo avesse toccato non potrebbe essere considerata morta.
Altri esempi di codice morto:
if (false) { dead_code(); }Questo strumento è disponibile sotto una doppia licenza: una commerciale adatta a progetti closed source e una licenza GPL utilizzabile in software open source.
A seconda delle tue esigenze, devi sceglierne una e seguirne le politiche. Un dettaglio delle politiche e degli accordi per ciascun tipo di licenza è disponibile nei file LICENSE.COMMERCIAL e LICENSE.GPL.