
Máquina virtual de Android y desofuscador
Simplify ejecuta virtualmente una aplicación para entender su comportamiento y luego intenta optimizar el código para que se comporte de manera idéntica pero sea más fácil de entender para un humano. Cada tipo de optimización es simple y genérico, por lo que no importa qué tipo específico de ofuscación se utilice.
El código de la izquierda es una descompilación de una aplicación ofuscada, y el código de la derecha ha sido desofuscado.
Hay tres partes en el proyecto: smalivm, simplify y la aplicación demo.
if o switch con un valor desconocido hace que se tomen ambas ramas.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
La construcción requiere que esté instalado el Java Development Kit 8 (JDK).
Debido a que este proyecto contiene submódulos para frameworks de Android, clone con --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
O actualice los submódulos en cualquier momento con:
git submodule update --init --recursive
Luego, para construir un único jar que contenga todas las dependencias:
./gradlew fatjar
El jar de Simplify estará en simplify/build/libs/. Puede probar que funciona simplificando la aplicación de ejemplo ofuscada proporcionada. Así es como se ejecuta (puede que necesite cambiar simplify.jar):
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
Para entender qué se está desofuscando, consulte el README de la Aplicación Ofuscada.
Si Simplify falla, pruebe estas recomendaciones, en orden:
-it.--max-address-visits, --max-call-depth y --max-method-visits.-v o -v 2 e informe el problema con los registros y un hash del DEX o APK.Si construye en Windows y la construcción falla con un error similar a:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Esto significa que Gradle no puede encontrar una ruta JDK adecuada. Asegúrese de que el JDK esté instalado, establezca la variable de entorno JAVA_HOME en su ruta JDK y asegúrese de cerrar y volver a abrir el símbolo del sistema que usa para construir.
No seas tímido. Creo que la ejecución virtual y la desofuscación son problemas fascinantes. Cualquiera que esté interesado es automáticamente genial y las contribuciones son bienvenidas, incluso si es solo para corregir un error tipográfico. No dudes en hacer preguntas en los issues y enviar pull requests.
Incluya un enlace al APK o DEX y el comando completo que está utilizando. Esto hace que sea mucho más fácil reproducir (y por lo tanto corregir) su problema.
Si no puede compartir la muestra, por favor incluya el hash del archivo (SHA1, SHA256, etc).
Si una operación coloca un valor de un tipo que puede convertirse en una constante como una cadena, número o booleano, esta optimización reemplazará esa operación con la constante. Por ejemplo:
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
En este ejemplo, una cadena cifrada se descifra y se coloca en v0. Dado que las cadenas son "constanteables", el move-result v0 puede reemplazarse 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."
El código está muerto si eliminarlo no puede alterar el comportamiento de la aplicación. El caso más obvio es si el código es inalcanzable, p.ej. if (false) { // dead }). Si el código es alcanzable, puede considerarse muerto si no afecta ningún estado fuera del método, es decir, no tiene efecto secundario. Por ejemplo, el código puede no afectar el valor de retorno del método, alterar ninguna variable de clase o realizar ninguna E/S. Esto es difícil de determinar en el análisis estático. Afortunadamente, smalivm no tiene que ser inteligente. Simplemente ejecuta estúpidamente todo lo que puede y asume que hay efectos secundarios si no puede estar seguro. Considere el ejemplo de Propagación 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."
En este código, el invoke-static ya no afecta el valor de retorno del método y supongamos que no hace nada extraño como escribir bytes en el sistema de archivos o en un socket de red, por lo que no tiene efectos secundarios. Simplemente puede eliminarse.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Finalmente, el primer const-string asigna un valor a un registro, pero ese valor nunca se usa, es decir, la asignación está muerta. También puede eliminarse.
const-string v0, "Tell me of your homeworld, Usul."
¡Hurra!
Uno de los principales desafíos del análisis estático de Java es la reflexión. Simplemente no es posible conocer los argumentos de los métodos de reflexión sin realizar un análisis cuidadoso del flujo de datos. Hay formas inteligentes y astutas de hacer esto, pero smalivm lo hace simplemente ejecutando el código. Cuando encuentra una invocación de método reflejado como:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Puede conocer los valores de v0, v1 y v2. Si está seguro de cuáles son los valores, puede reemplazar la llamada a Method.invoke() con una invocación de método real no reflejada. Lo mismo aplica para las búsquedas de campos y clases reflejadas.
Para todo lo que no encaja claramente en una categoría particular, existen las optimizaciones de mirilla. Esto incluye eliminar operaciones check-cast inútiles, reemplazar llamadas a Ljava/lang/String;-><init> con const-string, y así sucesivamente.
.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
Todo esto hace es 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
El move-result v0 se reemplaza con const/4 v0, 0x1. Esto se debe a que solo hay un posible valor de retorno para intValue()I y el tipo de retorno puede convertirse en una constante. Los argumentos v0 y v1 son inequívocos y no cambian. Es decir, hay un consenso de valores para cada posible ruta de ejecución en intValue()I. Otros tipos de valores que pueden convertirse 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
Debido a que el código anterior a const/4 v0, 0x1 no afecta el estado fuera del método (sin efectos secundarios), puede eliminarse sin cambiar el comportamiento. Si hubiera una llamada a método que escribiera algo en el sistema de archivos o la red, no podría eliminarse porque afecta el estado fuera del método. O si test()I tomara un argumento mutable, como un LinkedList, cualquier instrucción que lo accediera no podría considerarse muerta.
Otros ejemplos de código muerto:
if (false) { dead_code(); }Esta herramienta está disponible bajo una licencia dual: una comercial adecuada para proyectos de código cerrado y una licencia GPL que puede usarse en software de código abierto.
Dependiendo de sus necesidades, debe elegir una de ellas y seguir sus políticas. Un detalle de las políticas y acuerdos para cada tipo de licencia está disponible en los archivos LICENSE.COMMERCIAL y LICENSE.GPL.