
Виртуальная машина Android и деобфускатор
Simplify виртуально исполняет приложение, чтобы понять его поведение, а затем пытается оптимизировать код так, чтобы он вел себя идентично, но был понятнее для человека. Каждый тип оптимизации прост и универсален, поэтому не имеет значения, какой именно тип обфускации используется.
Код слева — это декомпиляция обфусцированного приложения, а код справа — после деобфускации.
Проект состоит из трех частей: smalivm, simplify и демонстрационное приложение.
if или switch с неизвестным значением приводит к выполнению обеих ветвей.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
Для сборки требуется установленный Java Development Kit 8 (JDK).
Поскольку проект содержит подмодули для Android-фреймворков, клонируйте с --recursive:
git clone --recursive https://github.com/CalebFenton/simplify.git
Или обновите подмодули в любое время с помощью:
git submodule update --init --recursive
Затем, чтобы собрать единый jar-файл, содержащий все зависимости:
./gradlew fatjar
Jar-файл Simplify будет находиться в simplify/build/libs/. Вы можете проверить его работу, упрощая предоставленное обфусцированное демонстрационное приложение. Вот как его запустить (возможно, потребуется изменить путь к simplify.jar):
java -jar simplify/build/libs/simplify.jar -it "org/cf/obfuscated" -et "MainActivity" simplify/obfuscated-app.apk
Чтобы понять, что именно деобфусцируется, ознакомьтесь с README обфусцированного приложения.
Если Simplify не работает, попробуйте следующие рекомендации по порядку:
-it.--max-address-visits, --max-call-depth и --max-method-visits.-v или -v 2 и сообщите о проблеме, приложив логи и хеш DEX или APK.Если сборка на Windows завершается ошибкой, похожей на:
Could not find tools.jar. Please check that C:\Program Files\Java\jre1.8.0_151 contains a valid JDK installation.
Это означает, что Gradle не может найти корректный путь к JDK. Убедитесь, что JDK установлен, задайте переменную окружения JAVA_HOME с путем к JDK и обязательно закройте и снова откройте командную строку, которую используете для сборки.
Не стесняйтесь. Я считаю, что виртуальное выполнение и деобфускация — увлекательные задачи. Любой, кто интересуется, автоматически крут, и вклад приветствуется, даже если это просто исправление опечатки. Не стесняйтесь задавать вопросы в issues и отправлять pull request'ы.
Пожалуйста, приложите ссылку на APK или DEX и полную используемую команду. Это значительно облегчает воспроизведение (и, следовательно, исправление) вашей проблемы.
Если вы не можете поделиться образцом, пожалуйста, укажите хеш файла (SHA1, SHA256 и т.д.).
Если операция помещает значение типа, которое может быть преобразовано в константу (например, строка, число или булево), эта оптимизация заменяет такую операцию на константу. Например:
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
В этом примере зашифрованная строка расшифровывается и помещается в v0. Поскольку строки "константизируемы", move-result v0 можно заменить на 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."
Код считается мертвым, если его удаление не может изменить поведение приложения. Самый очевидный случай — если код недостижим, например, if (false) { // dead }). Если код достижим, он может считаться мертвым, если он не влияет ни на какое состояние вне метода, то есть не имеет побочных эффектов. Например, код может не влиять на возвращаемое значение метода, не изменять переменные класса и не выполнять ввод/вывод. Это сложно определить при статическом анализе. К счастью, smalivm не нужно быть умным. Он просто тупо выполняет все, что может, и предполагает наличие побочных эффектов, если не уверен. Рассмотрим пример из раздела о распространении констант:
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."
В этом коде invoke-static больше не влияет на возвращаемое значение метода, и предположим, что он не делает ничего странного, например, не записывает байты в файловую систему или сетевой сокет, поэтому у него нет побочных эффектов. Его можно просто удалить.
const-string v0, "VGVsbCBtZSBvZiB5b3VyIGhvbWV3b3JsZCwgVXN1bC4="
const-string v0, "Tell me of your homeworld, Usul."
Наконец, первый const-string присваивает значение регистру, но это значение никогда не используется, то есть присваивание мертво. Его также можно удалить.
const-string v0, "Tell me of your homeworld, Usul."
Ура!
Одна из основных проблем статического анализа Java — это отражение. Невозможно узнать аргументы для методов отражения без тщательного анализа потоков данных. Существуют умные хитроумные способы, но smalivm делает это простым выполнением кода. Когда он обнаруживает вызов отражённого метода, например:
invoke-virtual {v0, v1, v2}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
Он может узнать значения v0, v1 и v2. Если он уверен в значениях, он может заменить вызов Method.invoke() на фактический вызов метода без отражения. То же самое относится к поиску отражённых полей и классов.
Для всего, что не вписывается четко в какую-либо категорию, существуют optimizations "глазка". Сюда входит удаление бесполезных операций check-cast, замена вызовов Ljava/lang/String;-><init> на const-string и так далее.
.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
Всё, что он делает — это 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 заменяется на const/4 v0, 0x1. Это происходит потому, что существует только одно возможное возвращаемое значение для intValue()I, и тип возврата может быть сделан константой. Аргументы v0 и v1 однозначны и не изменяются. То есть для всех возможных путей выполнения в intValue()I существует консенсус значений. Другие типы значений, которые могут быть преобразованы в константы:
const/4, const/16 и т.д.const-stringconst-class.method public static test1()I
.locals 2
const/4 v0, 0x1
return v0
.end method
Поскольку код выше const/4 v0, 0x1 не влияет на состояние вне метода (нет побочных эффектов), его можно удалить без изменения поведения. Если бы был вызов метода, который что-то записывает в файловую систему или сеть, его нельзя было бы удалить, потому что он влияет на состояние вне метода. Или если бы test()I принимала изменяемый аргумент, например LinkedList, любые инструкции, обращающиеся к нему, не могли бы считаться мертвыми.
Другие примеры мертвого кода:
if (false) { dead_code(); }Этот инструмент доступен под двойной лицензией: коммерческой, подходящей для закрытых проектов, и лицензией GPL, которая может использоваться в открытом программном обеспечении.
В зависимости от ваших потребностей вы должны выбрать одну из них и следовать ее политике. Подробности политики и соглашений для каждого типа лицензии доступны в файлах LICENSE.COMMERCIAL и LICENSE.GPL.