Реверс Java и Android

Как читается скомпилированный Java-код и Android-приложение, и что должен сделать автор, чтобы не отдать аналитику всё на блюде.

Байт-код JVM — промежуточный код в .class-файлах, который выполняет виртуальная машина Java. Как и в .NET, он сохраняет имена и сигнатуры, поэтому декомпиляторы восстанавливают по нему читаемый Java. Android добавляет свой формат: код упаковывается в .dex внутри APK.

Зачем это знать защитнику

Android-приложение — это архив, который скачивается на миллионы устройств и оказывается полностью в руках любого желающего. Аналитик безопасности (и злоумышленник) распаковывает APK и читает логику, ищет зашитые ключи, эндпоинты, флаги отладки. Если вы выпускаете мобильное приложение, понимать этот процесс обязательно: иначе вы оставите в коде секрет от платёжного шлюза или «секретный» URL админки, считая, что бинарник их прячет. Защитный взгляд: знать, что именно аналитик достанет из вашего APK, чтобы заранее это убрать.

Java: .class, .jar и декомпиляция

Компилятор javac переводит .java в .class с байт-кодом JVM. Несколько классов и ресурсов упаковывают в .jar — это обычный ZIP-архив. Поскольку .class хранит имена методов, полей и типов, декомпиляторы (CFR, Procyon, Fernflower, графический JD-GUI) восстанавливают близкий к оригиналу Java-код. Дизассемблер из JDK показывает байт-код СВОЕГО класса штатно:

# посмотреть байт-код своего класса (учебно)
javap -c -p MyClass.class

Фрагмент байт-кода — имена сохранены, поэтому смысл читается:

public boolean checkPin(java.lang.String);
  Code:
     0: aload_1
     1: ldc           "1234"     // зашитая строка-константа
     3: invokevirtual #2  // Method java/lang/String.equals
     6: ireturn

Видно зашитый PIN. Декомпилятор перепишет это в return pin.equals("1234");. Вывод тот же, что и в .NET: зашитые секреты в управляемом байт-коде не спрятаны.

Android: APK, dex и smali

APK — тоже ZIP-архив. Внутри: classes.dex (код в формате Dalvik Executable), AndroidManifest.xml (компоненты, разрешения), папка res/ и resources.arsc (ресурсы и строки), нативные библиотеки в lib/. Android исполняет не JVM-байткод напрямую, а dex — оптимизированный формат для рантайма ART/Dalvik, где все классы собраны в один файл.

Чтобы разобрать APK, аналитики используют распаковку и преобразование форматов — как иллюстрация метода в лаборатории на СВОЁМ приложении:

# распаковать ресурсы и получить smali (свой APK, учебно)
apktool d myapp.apk -o myapp_src

# конвертировать dex в jar и открыть декомпилятором
d2j-dex2jar myapp.apk -o myapp.jar

smali — это человекочитаемый ассемблер для dex-байткода (то, что выдаёт apktool). По нему видна структура классов и методов; инструмент jadx идёт дальше и пытается восстановить уже Java-исходник. Фрагмент smali узнаваем:

.method public getApiKey()Ljava/lang/String;
    .registers 2
    const-string v0, "sk_live_secret_xxx"   # зашитый ключ виден в открытую
    return-object v0
.end method

Здесь хорошо видно главное правило: строковые константы в dex лежат фактически открытым текстом, и даже без полноценной декомпиляции их вытащит простой просмотр строк.

Что аналитик извлекает

Из обычного APK без какой-либо защиты типично достают: зашитые ключи и токены, адреса бэкенд-эндпоинтов, имена и логику внутренних проверок (антифрод, лицензия, фиче-флаги), параметры запросов, иногда — тестовые учётки и debug-флаги, забытые перед релизом. Всё это — материал и для пентест-отчёта, и для злоумышленника, поэтому задача автора — чтобы извлекать было нечего.

Как это работает под капотом

JVM и ART — стековые/регистровые виртуальные машины, выполняющие байт-код, который ссылается на классы и методы по символическим именам через таблицу констант (constant pool). Эти имена обязаны присутствовать, чтобы рантайм связал вызовы — поэтому удалить их полностью нельзя, можно лишь заменить осмысленные на короткие бессмысленные. Декомпилятор читает таблицу констант и структуру методов, моделирует стек/регистры, сворачивает последовательности инструкций в выражения и распознаёт управляющие конструкции по переходам — отсюда читаемый результат. dex отличается от JVM-классов компоновкой (один файл, общий пул строк и типов, регистровая модель вместо стековой), что и делает строки особенно легко извлекаемыми: они собраны в одной секции. Инструменты вроде jadx объединяют разбор dex, восстановление управляющего потока и эвристики именования.

Как защититься

Чек-лист автора Android/Java-приложения.

  • Включите R8/ProGuard. Они переименовывают (минифицируют) классы и методы и удаляют неиспользуемый код, усложняя чтение. Это базовый, но важный слой.
  • Секреты — не в коде и не в ресурсах. API-ключи, пароли, токены не зашивайте в исходник, strings.xml или BuildConfig: всё это извлекается. Секреты держите на сервере, клиент получает их по аутентифицированному запросу.
  • Серверная авторизация. Любые проверки прав/подписки дублируйте на бэкенде — клиентскую логику легко вырезать в smali.
  • Уберите debug-артефакты перед релизом. Логи с чувствительными данными, тестовые учётки, флаг debuggable, отладочные эндпоинты.
  • Защита транспорта. TLS обязателен; для критичных приложений рассматривают certificate pinning, чтобы усложнить перехват трафика.
  • Нативный код и анти-тампер по необходимости. Вынос самой чувствительной логики в нативную библиотеку и проверка целостности подписи повышают планку, но не отменяют серверной защиты.

Юридическое напоминание: разбирать чужие приложения можно только в рамках закона и условий использования — для своего кода, разрешённого аудита или анализа вредоносного образца в изолированной среде.

Итоги

  • Байт-код JVM (.class/.jar) хранит имена — декомпиляторы (CFR, Fernflower, JD-GUI) восстанавливают близкий Java.
  • APK — это ZIP с classes.dex; apktool даёт smali, jadx — Java-подобный исходник.
  • Строки в dex лежат почти открытым текстом — зашитые ключи и эндпоинты извлекаются тривиально.
  • Защита: R8/ProGuard, секреты только на сервере, серверная авторизация, чистка debug-артефактов, TLS/pinning.
  • Анализ чужих приложений — только легально; клиент в принципе не место для секретов.
Проверьте себя
1. Что представляет собой файл APK с точки зрения формата?
AЗашифрованный контейнер, который нельзя открыть без ключа Google
BZIP-архив, содержащий classes.dex, манифест и ресурсы
CОдин скомпилированный нативный исполняемый файл
DТекстовый файл с исходниками на Kotlin
2. Почему зашивать API-ключ в strings.xml или BuildConfig Android-приложения небезопасно?
AЭти файлы шифруются, но ключ от шифрования утекает
BСтроковые ресурсы и константы извлекаются из APK тривиально (apktool/jadx/просмотр строк)
CAndroid запрещает строки длиннее 16 символов
DКлюч виден только при наличии root на устройстве
3. Что из перечисленного — корректный защитный шаг для Android-приложения?
AСчитать, что компиляция в dex уже прячет логику
BВключить R8/ProGuard, вынести секреты на сервер и дублировать авторизацию на бэкенде
CХранить пароль администратора в smali, потому что его трудно читать
DОтключить TLS ради скорости