
Привет, Хабр! На связи Александр Ананикян, аналитик сервиса по защите мобильных приложений от реверс‑инжиниринга. Мы в команде PT MAZE делаем «кнопку», которая превращает мобильные приложения в самый запутанный лабиринт для хакера.
Защита мобильного приложения от реверс‑инжиниринга и взлома — обязательный этап, который стоит учитывать при разработке релизной сборки. Протекторы используют обфускацию (запутывание), шифрование и другие техники, чтобы затруднить статический анализ и защитить код от модификаций злоумышленниками. Но у многих разработчиков присутствует страх сломать приложение применением таких техник.
Чтобы развеять опасения разработчиков и повысить их уверенность в качестве защищённой сборки, мы покажем принципы работы с протекторо��. Нельзя же включать все техники по умолчанию в надежде, что функциональность не пострадает.
Далее мы разберем, какие участки Android‑приложения можно защитить одной кнопкой, что требует настройки, а что трогать нельзя.
Простые преобразования
Сперва рассмотрим техники, которые не требуют настройки. В современных протекторах такие преобразования могут группироваться в базовые пресеты защитных конфигураций и включаться одной кнопкой. Результат — защищенное приложение без усилий со стороны разработки.
Шифрование нативных библиотек
Приложения могут содержать библиотеки с важной бизнес‑логикой. Например, управление записями в базе данных. Такие нативные библиотеки можно зашифровать и запускать только в проверенной безопасной среде. Это подходит как для собственных библиотек C/C++, так и для сторонних .so, встроенных через third‑party‑зависимости. Кроме того, можно защищать нативные компоненты кросс‑платформенных фреймворков, таких как Flutter, React Native или Unity.
Ниже представлен пример использования библиотеки SQLCipher — хакеры часто анализируют ее из‑за того, что через нее в рантайме передаются ключи для получения доступа к зашифрованной базе данных.
После шифрования нативных библиотек внутри APK декомпилятор больше не может корректно разобрать файл. Если в исходной библиотеке Ghidra видит структуру, код и символы, то в зашифрованной библиотеке отсутствуют секции кода и данных и не обнаруживаются функции, импорты, экспорты и пространства имен.


До шифрования декомпилятору jadx доступны все библиотеки приложения в виде отдельных .so‑файлов. После шифрования нативные библиотеки упаковываются в единый файл libK.so.

Шифрование строк
Строкам в байт‑коде следует уделять особое внимание. Хакер без труда способен найти в них URL и пути API, токены доступа, ключи API и другую информацию. Cтроки также удобно использовать как ориентир в коде при анализе бизнес‑логики и создании вредоносных клонов.
Шифрование строк затрудняет реверс‑инжиниринг: вместо открытых значений
в коде остаются только зашифрованные блоки, а значит, исследование становится труднее. Строковые значения шифруются на этапе сборки приложения и расшифровываются только во время запуска на устройстве при условии, что проверки безопасности окружения пройдены. Это происходит незаметно для пользователя, при этом ключи не остаются в открытом виде в памяти. Заметного влияния на производительность эта техника не оказывает.

Шифрование файлов
Шифрование файлов (картинок, моделей, сертификатов и тому подобное) происходит по такому же принципу, как шифрование нативных библиотек. Их расшифровка выполняется во время работы приложения после проверки безопасности окружения. При анализе такие файлы выглядят как непонятный набор байтов — без ключа вытащить из них что‑то полезное невозможно.


Преобразование данных и инструкций
Преобразование данных и инструкций — это техника, которая усложняет анализ, но не меняет поведение приложения. Например, вместо того чтобы хранить число 100 в коде напрямую, его можно закодировать в виде математического выражения, результат которого все равно будет 100. Или можно добавить в байт‑код лишние инструкции, которые не делают ничего полезного, но усложняют чтение и разбор.
Такие изменения безопасны потому, что на работу приложения они не влияют, просто делают код объемнее и запутаннее. Исключения возможны разве что на слабых устройствах, где каждое вычисление имеет вес.
Где защита требует настройки
Для использования продвинутых техник защиты вам потребуются их тонкая настройка и понимание механизмов работы. Разберем случаи, где защита требует особой осторожности и внимания. Если вы не хотите сломать функциональность приложения, снизить его производительность или вызвать падения в непредсказуемых местах, примеры ниже помогут вам разобраться, что к чему.
Преобразование имен в байт‑коде
Переименование имен классов, методов, полей — один из подходов, который скрывает оригинальные названия, но сохраняет структуру и поведение приложения. На рисунке ниже показан пример использования таких техник. Фиолетовым выделены преобразования имен классов, красным — имена полей, синим — имена методов.

Важно:
Нельзя переименовывать классы и методы, которые вызываются по строковым именам: через рефлексию, JavaScript‑интерфейсы, JNI или строки в
AndroidManifest.xml(например,BroadcastReceiver,Service,ContentProvider). Если их имена изменятся, а ссылки останутся прежними, система или приложение не смогут найти нужный компонент — это приведет к сбоям или потере функциональности.Чтобы переименование не мешало корректному восстановлению стектрейса, следует правильно настроить экспорт mapping‑файла и интеграцию с системами сбора крашей (RuStore Tracer, Firebase Crashlytics и другие аналоги). В этом случае обфускация остается безопасной и не мешает анализу сбоев.
Замена вызовов на непрямые
Существует несколько подходов к замене прямых вызовов на непрямые. Мы рассмотрим скрытие вызовов методов через рефлексию — одну из самых продвинутых и сложных техник обфускации, которая значительно усложняет исследователю возможность построить и проанализировать граф выполнения кода, понять, в каких конкретно местах производятся вызовы ключевой логики. Применять ее следует очень осторожно. Не все протекторы могут корректно поддерживать одновременное применение рефлексии и переименования классов, полей и методов. При включении обеих техник есть шанс сломать вызовы через строки. Нам потребовалось немало сил, чтобы реализовать гарантированную поддержку рефлексии одновременно с переименованием.



При корректной настройке этих защитных техник можно дополнительно включить шифрование строк. В этом случае будет не только искажение CFG, но и фактически скрытие имен вызываемых методов.

переименования классов, методов, полей + шифрования строк
При использовании рефлексии разработчику важно указать разрешенные и запрещенные для модификации классы. Это делается с помощью двух списков.
Белый список
В больших приложениях предпочтительнее использовать whitelist — перечисление классов, которые следует защищать. Такой подход дает контроль над областью применения защиты: можно сделать несколько сборок, заранее их протестировать и, убедившись в работоспособности, спокойно пропустить в релиз.
При использовании рефлексии лучше ограничиться защитой только бизнес‑логики. Если покрытие слишком большое, то возникают потери производительности, особенно в больших приложениях. Кроме того, при неправильном использовании возможны неожиданные падения, неопределенное поведение из‑за того, что протектор переименовывает методы или классы, обращения к которым происходят по их строковым именам.
Черный список
Этот список имеет смысл заполнять, когда в приложении есть классы, которые нельзя обфусцировать, или когда нельзя заменять ��рямые вызовы на непрямые без риска поломки. Это актуально для небольших и простых приложений, где такие классы можно перечислить вручную.
Следует помнить, что рефлексия ломает корутины и многопоточность. Чтобы избежать этого, следует явно исключать классы kotlin.coroutines и внимательно фильтровать цепочки вызовов, которые могут привести к падениям.
Можно также смело исключать классы, связанные с UI‑компонентами: хакерам они не интересны. Но если рефлексия затрагивает элементы пользовательского интерфейса (View, Fragment и тому подобное), то это может замедлить работу приложения и ухудшить отзывчивость интерфейса.
Дополнительно следует учитывать, что рефлексия может нарушить сетевую функциональность при работе с сокетами. Там, где время отклика значимо, любые вмешательства в код могут привести к сбоям или нестабильной работе, поэтому такие классы тоже должны попасть в исключения.
Шифрование ресурсов
Шифрование ресурсов — достаточно безопасная практика, которая позволяет преобразовать идентификаторы и их значения в нечитаемые, чтобы скрыть содержимое и затруднить анализ. Но при такой обработке важно сохранить нетронутыми определенные ресурсы — например, идентификаторы, используемые Firebase‑ и Google‑сервисами (google_api_key, google_app_id и другие), а также ресурсы, на которые ссылается SDUI, поскольку переименование таких элементов может нарушить корректную работу приложения.
Идентификаторы, используемые Google‑ и Firebase‑сервисами
В Android‑приложениях с Firebase‑ и Google‑сервисами автоматически генерируются ресурсы в res/values/strings.xml.


Эти строки используются Firebase SDK для инициализации приложения и доступа к сервисам. Firebase ожидает найти в приложении именно такие имена ресурсов, поэтому изменять их нельзя. Сами значения при этом шифровать безопасно: нельзя трогать только их имена.
Ресурсы, используемые в SDUI
В приложениях с SDUI интерфейс строится на лету — из данных, которые приходят с сервера. Важно не переименовывать строки, стили и layout‑ресурсы, если обращение к ним происходит по имени. Например, сервер может прислать такой JSON‑файл.
Листинг 1. Фрагмент JSON‑файла
{ “type”: “button”, “title”: “login_button_text”, “action”: “submitForm” }
Клиент должен отобразить кнопку и найти для нее заголовок — по строковому имени login_button_text, а не через обычный R.string. Для этого вызывается getIdentifier().
Листинг 2. Фрагмент кода на стороне пользователя
val title = context.getResources().getString( context.resources.getIdentifier(“login_button_text”, “string”, context.packageName) )
Если протектор переименует login_button_text в a1, то приложение больше не найдет нужную строку. Результат — пустая кнопка, неработающий экран или краш.
Шифрование всего байт‑кода приложения
Шифрование всего байт‑кода — техника, при которой все .dex‑файлы упаковываются в зашифрованном виде и загружаются в память только во время запуска через собственный ClassLoader, чтобы скрыть логику приложения. Это преобразование эффективно защищает приложение от реверс‑инжиниринга: статический анализ затрудняется, сканеры и декомпиляторы не могут получить читаемый код. Хакер видит пустой код, а при попытке открыть файл с зашифрованным содержимым без ключа обнаруживает нечитаемый набор байтов.
Такая защита требует осторожного применения: расшифровка и загрузка кода в рантайме требуют ресурсов — это увеличивает использование памяти и время запуска приложения. Техника подойдет небольшим приложениям и не рассчитана на слабые устройства. Для остальных случаев нужна настройка .dex‑файлов с помощью черных или белых списков классов.
Обфускация потока выполнения кода
Обфускация control flow — это изменение внутренней логики метода так, чтобы сохранить поведение, но скрыть реальную структуру. Например, можно применить control‑flow flattening или заменить прямые вызовы на непрямые.
Важно
Техника считается безопасной, если заранее учтены исключения — особенно в коде, где происходят обращения к классам, методам или полям по строковым именам в рантайме. В таких случаях важно исключить чувствительные участки из преобразований, иначе приложение может не найти нужные элементы во время выполнения.
Виртуализация кода
Виртуализация — это техника защиты, при которой исходный байт‑код приложения преобразуется в новый набор инструкций, понятный только встроенной виртуальной машине протектора. Набор инструкций и виртуальная машина генерируются заново при каждой сборке и не похожи на предыдущие.
Главная проблема виртуализации — производительность. Код выполняется через интерпретатор, а каждая виртуальная инструкция развертывается в десятки реальных операций. Это во много раз увеличивает нагрузку на CPU, энергопотребление и задержки, поэтому виртуализацию применяют только к небольшим, критически значимым участкам кода и всегда проверяют влияние на скорость.
Виртуализация также требует поддержки собственной виртуальной машины поверх уже существующей виртуальной машины Android. Все это делает ее дорогой в разработке, что сказывается и на цене коммерческих протекторов.
Виртуализация — одна из самых продвинутых и сложных в настройке техник, поэтому применять ее следует, только когда более простые техники защиты уже внедрены и успешно работают.
Что нельзя трогать при защите приложения
Есть участки приложения, которые должны оставаться в исходном виде. Любые изменения в таких местах приведут к некорректному поведению или нестабильной работе. Здесь мы собрали исключения, которые всегда нужно учитывать при настройке защиты.
Экспортируемые компоненты в AndroidManifest.xml
Имена экспортируемых компонентов с <intent‑filter> или android:exported=«true» менять нельзя — иначе система не найдет их при запуске или вызове из другого приложения. Но есть исключения: MainActivity и внутренние Activity можно переименовывать при синхронизации с AndroidManifest.xml. Ограничения в первую очередь касаются IPC‑компонентов, таких как ContentProvider, а также экспортируемых Service и BroadcastReceiver, имена которых используются внешними приложениями или системой.
Публичные API
Публичные API, к которым есть доступ вне приложения, трогать н��льзя. Один из примеров — методы с аннотацией @JavascriptInterface в WebView. Если их переименовать, то WebView не найдет нужный метод по имени, тогда JavaScript‑вызов сломается. ProGuard по умолчанию это учитывает и запрещает такие переименования, поэтому для простой, быстрой и эффективной настройки можно использовать его до применения протектора.
Листинг 3. Фрагмент правила proguard‑rules.pro
class: ‑keepclassmembers class fqcn.of.javascript.interface.for.webview { public *; }
Методы с модификатором native
Метод с модификатором native нельзя переименовывать, потому что его связь с нативной реализацией в .so‑библиотеке зависит от точного имени. JVM формирует имя нативной функции по имени Java‑метода. Если его изменить, то связь нарушится, и метод не найдется при вызове через JNI.
Листинг 4. Фрагмент Java‑метода
private native String getSomething(int i);
JVM будет искать в .so‑библиотеке имя C‑функции строго определенного формата.
Листинг 5. Имя C‑функции по правилам JNI
Java_com_example_MyClass_getSomething
Если протектор переименует Java‑метод в q(), но нативная библиотека останется прежней, то связь нарушится, и метод не будет найден. Это приведет к крашу приложения.
О чем следует всегда помнить
Мы рассмотрели большое число техник и выделили их в следующие категории:
Простые преобразования, которые значительно повышают безопасность приложения. Их можно включить, нажав одну кнопку.
Продвинутые техники защиты, которые с должной настройкой способны вывести безопасность приложения на новый уровень. Однако их применение уже требует некоторых знаний и навыков, иначе есть риск что‑то сломать.
Жесткие ограничения для определенных участков приложения. Вмешательство в них гарантированно сломает приложение.
Надеемся, что эта статья развеяла еще один миф, что протекторы ломают Android‑приложения. На деле все зависит от того, как ими пользоваться.
Мы, команда PT MAZE, потратили много сил на то, чтобы сделать внедрение мощной защиты мобильных приложений максимально удобным, безболезненным и безопасным, и готовы помочь быстро пройти этот путь и вам.
