Под капотом у DexProtector

Итак, мне достался зашифрованный APK.

Внутри — маленький classes.dex, в assets — classes.dex.dat (вероятно, результат зашифровки исходного dex), dp.<arch>.so.dat (видимо, зашифрованные JNI, которые призваны расшифровать что-нибудь), resources.dat, а также некоторые файлы, которые, скорее всего, лежали в assets до зашифровки, — но размером в 32 байта каждый (может, индексы/указатели).

Первый уровень

В APK/classes.dex всего один класс: com.company.app.ProtectedApplicationName.

Dex декомпилируется с помощью dex2jar лишь частично: один из методов слишком большой, d2j его игнорирует. Но jar на выходе всё же имеется, распаковываю его и декомпилирую единственный .class встроенным в IDEA Fernflower.

В коде имеется пара методов с бессмысленными именами, а также несколько перегрузок метода onCreate. @Override void onCreate(), String onCreate(String), int onCreate()… Стоп! Методы void onCreate() и int onCreate() в языке Java не могут сосуществовать в одном классе, т. к. Java-сигнатура обоих — onCreate()! Несомненно, это разные методы с точки зрения JVM (onCreate()V и onCreate()I), спецификация разрешает это ради ковариантных возвращаемых значений, но такой байт-код невозможно получить непосредственно с помощью javac. Для этого использовались какие-то дополнительные инструменты. В крайнем случае, такой код можно было написать прямо на smali.

Перейдём к smali-коду метода, который не удалось перевести в JVM-байт-код с помощью dex2jar. Самая длинная его часть — содержимое некоего массива байт. Дело в том, что в smali-классах, в отличие от Java-классов, нет пула констант. Строки и массивы находятся прямо внутри smali, а пул создаётся один на весь dex. Поэтому и содержимое массива находится прямо в методе.

Сам метод состоит по большей части из конструкций вида Class.forName(onCreate("иероглифы")) и varX.getMethod(onCreate("каракули"), varY), и это как бы намекает на то, что метод String onCreate(String) расшифровывает строки. При их расшифровке в качестве одного из операндов используется хэш-код строки, сложенной из имён методов, полученных с помощью Thread.currentThread().getStackTrace().

Занимается вышеуказанный (большой) метод преобразованием массива байт побитовыми операциями, сохранением его в файл по имени Long.toHexString(System.currentTimeMillis()) + ".apk" и загрузкой классов из него методом DexFile.loadDex(String sourcePathName, String outputPathName, int flags).

Хм, почему не PathClassLoader? Ладно, не моё дело.

Второй уровень

В маленьком файле <шестнадцатеричные-миллисекунды>.apk нет ничего кроме classes.dex. Он легко декомпилируется с помощью dex2jar. Там оказывается три класса, два из которых с лёгкостью декомпилируются в Java с помощью Fernflower. Третий класс удаётся декомпилировать другим декомпилятором (не помню, да и не важно), но он нежизнеспособен: множество методов с одинаковой Java-сигнатурой, выброс checked exceptions, локальный переменные типа void.

Имеют место неправильно преобразованные прыжки:

while (true) {
    return;
    break;
}

Сложно сказать, какой код способен выполниться, а какие участки существуют только для затруднения декомпиляции и отвода глаз.

Smali-код очень похож на код из предыдущего класса: Class.forName(onCreate("каракули")) и всё такое. И он так же зависим от собственного стека вызова, т. к. для расшифровки строк использует тот же метод String onCreate(String) из основного класса.

Выполнение заканчивается расшифровкой файла assets/dp.<arch>.so.dat в libdexprotector.so. Если же поломать процесс расшифровки неправильным стеком вызова, неправильной подписью пакета приложения или ещё невесть чем (перепакованное приложение не запустилось даже при подмене подписи), у so будет поломанный magic (и всё остальное), и нативная библиотека не загрузится.

Третий уровень

Потугами нативного кода создаётся файл <32 шестнадцатеричных цифры>.apk. В нём снова кроме classes.dex ничего нет. После декомпиляции dex уже можно почитать нормальные классы, но строки зашифрованы, например: doSomething(b.c.a.a.ffc("\u7aea\u6f9c\u976e\u737c\ud085\u3ab1\ua951\u3488\u51da\udf41\u9b9f\u8f77\u2b2e\u1143\uad41\u6fca\u61c2\u77de")).

При попытке выполнить в отладчике код Class.forName("com.company.app.b$c$a$a") я получил ClassNotFoundException. Возможно, при загрузке dex вышеуказанная строка подменялась другой, но точно я этого не знаю. В любом случае, расшифрованные строки можно подсмотреть в отладчике.

Итог

Что я получил

Я добрался до классов и имею возможность подсмотреть логику работы приложения. Кроме того, теперь я имею представление о том, как оно зашифровано.

Чего я не получил

  1. Я не расшифровал строки. Несомненно, потратив ещё некоторое время, можно это сделать, но я всегда могу посмотреть их значения в отладчике.
  2. Я не притронулся к libdexprotector.so. Мои навыки не позволяют мне посмотреть, что находится внутри ELF-файла.
  3. Я не разобрался, почему перепакованное приложение неверно расшифровывает libdexprotector.so и падает из-за неправильного magic, даже если подменить подпись. При этом обращений к MessageDigest или CRC32 я не обнаружил, так что, вероятно, дело не в замене самих файлов.
  4. Я не разобрался, зачем были вызовы к Runtime.getRuntime.exec("getprop", null, null).

  5. Я не расшифровал ресурсы. Не знаю, насколько это актуально, если используемые ресурсы можно подсмотреть через отладчик.

Вывод

Результат немного предсказуем. Любой код, который работает, можно подсмотреть. Любой специалист на зарплате при необходимости расковыряет такой APK гораздо дальше, чем сделал это я в свободные минуты без каких-либо навыков реверс-инжиниринга.

Инструменты и благодарности

Два чая автору smali/baksmali и smalidea — человеку по имени Ben Gruver (JesusFreke). Его продукт был моим основным инструментом.

Ещё два чая с синтаксическим сахаром — компании JetBrains в благодарность за IntelliJ IDEA / Android Studio.

Чай Ольге за помощь с вычиткой текста :)

Комментарии к статье