O Simplify executa virtualmente um aplicativo para entender seu comportamento e, em seguida, tenta otimizar o código para que ele se comporte de forma idêntica, mas seja mais fácil de ser entendido por um ser humano. Cada tipo de otimização é simples e genérico, portanto não importa qual tipo específico de ofuscação é usado.
O código à esquerda é uma descompilação de um aplicativo ofuscado e o código à direita foi desofuscado.
O projeto tem três partes: smalivm, simplificar e o aplicativo de demonstração.
if
ou switch
com um valor desconhecido resulta na tomada de ambas as ramificações. 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
A construção requer a instalação do Java Development Kit 8 (JDK).
Como este projeto contém submódulos para estruturas Android, clone com --recursive
:
git clone --recursive https://github.com/CalebFenton/simplify.git
Ou atualize os submódulos a qualquer momento com:
git submodule update --init --recursive
Então, para construir um único jar que contenha todas as dependências:
./gradlew fatjar
O jar do Simplify estará em simplify/build/libs/
. Você pode testar se está funcionando simplificando o aplicativo de exemplo ofuscado fornecido. Veja como você o executaria (talvez seja necessário alterar simplify.jar
):
java -jar simplify/build/libs/simplify.jar -it " org/cf/obfuscated " -et " MainActivity " simplify/obfuscated-app.apk
Para entender o que está sendo desofuscado, verifique o README do aplicativo ofuscado.
Se o Simplify falhar, tente estas recomendações, na ordem:
-it
.--max-address-visits
, --max-call-depth
e --max-method-visits
.-v
ou -v 2
e relate o problema com os logs e um hash do DEX ou APK.Se a construção no Windows e a construção falharem com um erro semelhante a:
Não foi possível encontrar ferramentas.jar. Verifique se C:Program FilesJavajre1.8.0_151 contém uma instalação válida do JDK.
Isso significa que o Gradle não consegue encontrar um caminho JDK adequado. Certifique-se de que o JDK esteja instalado, defina a variável de ambiente JAVA_HOME
para o caminho do JDK e feche e reabra o prompt de comando que você usa para construir.
Não seja tímido. Acho que a execução virtual e a desofuscação são problemas fascinantes. Qualquer pessoa interessada é automaticamente legal e contribuições são bem-vindas, mesmo que seja apenas para corrigir um erro de digitação. Sinta-se à vontade para fazer perguntas nos problemas e enviar solicitações pull.
Inclua um link para o APK ou DEX e o comando completo que você está usando. Isso torna muito mais fácil reproduzir (e, portanto, corrigir ) seu problema.
Se você não puder compartilhar a amostra, inclua o hash do arquivo (SHA1, SHA256, etc).
Se uma operação colocar um valor de um tipo que possa ser transformado em uma constante, como string, número ou booleano, essa otimização substituirá essa operação pela constante. Por exemplo:
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
Neste exemplo, uma string criptografada é descriptografada e colocada em v0
. Como as strings são "constantizáveis", o move-result v0
pode ser substituído por uma 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."
O código está morto se removê-lo não puder alterar o comportamento do aplicativo. O caso mais óbvio é se o código estiver inacessível, por exemplo if (false) { // dead }
). Se o código estiver acessível, ele poderá ser considerado morto se não afetar nenhum estado fora do método, ou seja, não tiver efeito colateral . Por exemplo, o código não pode afetar o valor de retorno do método, alterar quaisquer variáveis de classe ou executar qualquer IO. Isto é difícil de determinar na análise estática. Felizmente, o smalivm não precisa ser inteligente. Ele simplesmente executa estupidamente tudo o que pode e assume que há efeitos colaterais se não tiver certeza. Considere o exemplo de 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."
Neste código, o invoke-static
não afeta mais o valor de retorno do método e vamos supor que ele não faz nada estranho, como gravar bytes no sistema de arquivos ou em um soquete de rede, portanto, não tem efeitos colaterais. Ele pode simplesmente ser removido.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Finalmente, a primeira const-string
atribui um valor a um registrador, mas esse valor nunca é usado, ou seja, a atribuição está morta. Também pode ser removido.
const-string v0, "Tell me of your homeworld, Usul."
Huza!
Um grande desafio da análise estática de Java é a reflexão. Simplesmente não é possível saber se os argumentos são para métodos de reflexão sem fazer uma análise cuidadosa do fluxo de dados. Existem maneiras inteligentes de fazer isso, mas o smalivm faz isso apenas executando o código. Quando encontra uma invocação de método refletida, como:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Ele pode saber os valores de v0
, v1
e v2
. Se tiver certeza de quais são os valores, ele poderá substituir a chamada para Method.invoke()
por uma invocação de método real não refletida. O mesmo se aplica a pesquisas de campos e classes refletidas.
Para tudo que não se enquadra perfeitamente em uma categoria específica, existem otimizações de olho mágico. Isso inclui a remoção de operações check-cast
inúteis, a substituição de chamadas Ljava/lang/String;-><init>
por const-string
e assim por diante.
.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
Tudo o que isso faz é 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
O move-result v0
é substituído por const/4 v0, 0x1
. Isso ocorre porque há apenas um valor de retorno possível para intValue()I
e o tipo de retorno pode se tornar uma constante. Os argumentos v0
e v1
são inequívocos e não mudam. Ou seja, há um consenso de valores para cada caminho de execução possível em intValue()I
. Outros tipos de valores que podem ser transformados em constantes:
const/4
, const/16
, etc.const-string
const-class
.method public static test1 () I
.locals 2
const/4 v0 , 0x1
return v0
.end method
Como o código acima const/4 v0, 0x1
não afeta o estado fora do método (sem efeitos colaterais), ele pode ser removido sem alterar o comportamento. Se houvesse uma chamada de método que gravou algo no sistema de arquivos ou na rede, ela não poderia ser removida porque afeta o estado fora do método. Ou se test()I
pegasse um argumento mutável, como LinkedList
, quaisquer instruções que o acessassem não poderiam ser consideradas mortas.
Outros exemplos de código morto:
if (false) { dead_code(); }
Esta ferramenta está disponível sob uma licença dupla: uma comercial adequada para projetos de código fechado e uma licença GPL que pode ser usada em software de código aberto.
Dependendo de suas necessidades, você deve escolher um deles e seguir suas políticas. Detalhes das políticas e contratos para cada tipo de licença estão disponíveis nos arquivos LICENSE.COMMERCIAL e LICENSE.GPL.