
Máquina virtual Android e desofuscador
O Simplify executa virtualmente um aplicativo para entender seu comportamento e então tenta otimizar o código para que se comporte de forma idêntica, mas seja mais fácil para um humano entender. 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.
Há três partes no projeto: smalivm, simplify e o aplicativo de demonstração.
if ou switch com um valor desconhecido resulta em ambos os ramos sendo seguidos.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 compilação requer a instalação do Java Development Kit 8 (JDK).
Como este projeto contém submodulos para frameworks Android, clone com --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
Ou atualize os submodulos a qualquer momento com:
git submodule update --init --recursive
Então, para construir um único jar que contém 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. Aqui está como executá-lo (você pode precisar 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, veja o README do Aplicativo Ofuscado.
Se o Simplify falhar, tente estas recomendações, em 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 estiver compilando no Windows e a compilação falhar com um erro semelhante a:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Isso significa que o Gradle não consegue encontrar um caminho adequado do JDK. Certifique-se de que o JDK está instalado, defina a variável de ambiente JAVA_HOME para o caminho do JDK e certifique-se de fechar e reabrir o prompt de comando usado para compilar.
Não seja tímido. Eu acho que execução virtual e desofuscação são problemas fascinantes. Qualquer um interessado é 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 nas issues e enviar pull requests.
Por favor, inclua um link para o APK ou DEX e o comando completo que você está usando. Isso torna muito mais fácil reproduzir (e assim corrigir) o seu problema.
Se você não puder compartilhar a amostra, por favor inclua o hash do arquivo (SHA1, SHA256, etc).
Se uma operação coloca um valor de um tipo que pode ser transformado em uma constante, como uma string, número ou booleano, esta 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 strings são "constantizáveis", o move-result v0 pode ser substituído por um 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 é inalcançável, ex.: if (false) { // morto }). Se o código é alcançável, pode ser considerado morto se não afetar nenhum estado fora do método, ou seja, não tem efeito colateral. Por exemplo, o código pode não afetar o valor de retorno do método, alterar variáveis de classe ou realizar E/S. Isso é difícil de determinar em análise estática. Felizmente, o smalivm não precisa ser inteligente. Ele apenas executa tudo que pode de forma estúpida e assume que há efeitos colaterais se não tiver certeza. Considere o exemplo da Propagação 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."
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 escrever bytes no sistema de arquivos ou em um socket de rede, então não tem efeitos colaterais. Ele pode ser simplesmente removido.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Finalmente, o primeiro const-string atribui um valor a um registrador, mas esse valor nunca é usado, ou seja, a atribuição está morta. Também pode ser removida.
const-string v0, "Tell me of your homeworld, Usul."
Eba!
Um grande desafio na análise estática de Java é a reflexão. Simplesmente não é possível saber quais são os argumentos para métodos de reflexão sem fazer uma análise cuidadosa do fluxo de dados. Existem maneiras inteligentes e engenhosas de fazer isso, mas o smalivm faz apenas executando o código. Quando encontra uma invocação de método refletido 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 sobre os valores, pode substituir a chamada a Method.invoke() por uma invocação de método real não refletida. O mesmo se aplica a buscas refletidas de campos e classes.
Para tudo que não se encaixa perfeitamente em uma categoria específica, existem otimizações de pequeno escopo. Isso inclui remover operações check-cast inúteis, substituir chamadas Ljava/lang/String;-><init> por const-string, e assim por diante.
.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
Tudo isso faz é 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
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 ser transformado em uma constante. Os argumentos v0 e v1 são inequívocos e não mudam. Isto é, 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-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
Porque o código acima de 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 escrevesse 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 recebesse um argumento mutável, como um 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 uma delas e seguir suas políticas. Um detalhe das políticas e acordos para cada tipo de licença está disponível nos arquivos LICENSE.COMMERCIAL e LICENSE.GPL.