
Краши в мобильных приложениях — это нормальная часть жизни продукта. Разработчики тратят много времени на то, чтобы находить причину падения приложения и исправлять её. Параллельно есть и другая задача: сделать код безопасным, чтобы усложнить взлом приложения.
Применение различных техник запутывания (обфускации) и упаковки кода повышает защиту от статического анализа, но может создавать новую трудность — делать стектрейс нечитаемым: разработчик видит не CrashApp(), а a.b.c().
Получается парадокс: приложение защищено, но код недоступен и для хакера, и для разработчика. И если не настроить восстановление стектрейса, то расследование крашей превращается в головоломку и слепому поиску причины падения приложения.
На практике сейчас существуют два способа переименовывать код.
1. ProGuard/R8 (без протектора).
Это класс инструментов оптимизации и переименования, который выполняется при стандартной сборке Android-проекта через Gradle-плагин. Он минимизирует код, генерирует mapping.txt и корректно работает с системами crash-аналитики.
2. ProGuard/R8 + протектор.
Это двухслойная защита: сначала ProGuard/R8 выполняет минимизацию и переименование, затем протектор обфусцирует байт-код и ресурсы приложения, обновляет существующий mapping-файл и возвращает его обратно.
И здесь возникает еще одна проблема: не все системы сбора крашей одинаково хорошо обрабатывают mapping-файлы после применения серьезных техник защиты. Сейчас чаще всего используются такие сервисы:
И если mapping.txt обработан некорректно, то эти системы не смогут восстановить стектрейс, и разработчик не увидит реальные места падения приложения.
Далее мы рассмотрим оба подхода, разберем их достоинства и ограничения, и покажем на практике, как правильно сохранять читаемость стектрейсов при включённой защите.
При использовании только ProGuard/R8 чтение стектрейса не вызывает трудностей. Эти инструменты выполняют стандартное переименование имён, создают mapping.txt в предсказуемом формате и автоматически интегрируются с системами краш-аналитики. Если прилетает ошибка, стектрейс легко восстанавливается. Например, получаем такой вид в отчёте:
Листинг 1. Фрагмент стектрейса до применения ProGuard/R8
Fatal Exception: java.lang.RuntimeException: Test crash from obfuscated Compose build at com.example.md5testapp.MainActivityKt.causeCrash(MainActivity.kt:41)
После применения ProGuard/R8 стектрейс может выглядеть так:
Листинг 2. Фрагмент стектрейса после применения ProGuard/R8
Fatal Exception: java.lang.RuntimeException: Test crash from obfuscated Compose build at j3.d.a(r8-map-id-5a58056302b510b22b077d2c72e831457b3d58f1b79baa8dc30cd786ac0c4ba6:39)
Но система краш-аналитики использует mapping.txt и автоматически возвращает исходные имена. Разработчик видит восстановленный стектрейс и может сразу исправить баг.
ProGuard/R8:
В открытом виде остаются:
Если значимая часть логики лежит в ресурсах, строках, то ProGuard/R8 её не скрывает. Эти значения дают достаточно подсказок современным декомпиляторам, и все важные данные сразу становятся видны хакеру.
Достоинства подхода:
Недостатки подхода:
ProGuard/R8 — это инструмент оптимизации и удобной сборки, а не инструмент защиты. Он ускоряет разработку и процесс доставки приложения пользователям, упрощает отладку. Это подходит, если вам пока рано думать о безопасности, и вы активно заняты развитием продукта. Но если нужна защита от реверса, то одного ProGuard/R8 мало.
При применении комбинированного подхода чтение стектрейса не вызывает проблем, при условии, что протектор корректно обрабатывает mapping.txt после применения своих техник запутывания и упаковки кода. Здесь важный момент: протектор не создаёт новый mapping-файл, а берёт существующий файл от ProGuard/R8, модифицирует байт-код и ресурсы приложения, исправляет mapping-файл и возвращает его обратно. В результате даже сильная обфускация не мешает диагностике ошибок.
Современные протекторы меняет не только имена классов, методов и полей, но и те элементы байт-кода, которые ProGuard/R8 не затрагивают. Ниже представлены примеры использования протектора PT MAZE поверх ProGuard/R8 на примере кода тестового приложения в декомпиляторе Jadx.
1. Аннотация @kotlin.jvm.internal.SourceDebugExtension
Например, ProGuard не обфусцирует аннотацию @kotlin.jvm.internal.SourceDebugExtension, которая автоматически добавляется компилятором Kotlin в версии 1.8. Это означает, что после сборки проекта класс с этой аннотацией остается с исходным именем. Такие метаданные становятся подсказкой для хакера.
На рисунках ниже показаны фрагменты поиска аннотации @kotlin.jvm.internal.SourceDebugExtension с включенным плагином Kotlin SMAP
в настройках декомпилятора.
Рисунок 1. Аннотация kotlin.jvm.internal.SourceDebugExtension после применения ProGuard/R8 без протектора
Современный протектор скрывает эту аннотацию от реверсера.
Рисунок 2. Аннотация kotlin.jvm.internal.SourceDebugExtension после применения ProGuard/R8 + протектора
2. Строковые значения в параметрах аннотаций
ProGuard/R8 переименовывает классы, методы и аннотации, включая @GET, @POST и @Headers. Но не изменяет строковые значения, переданные в параметры аннотаций, например, HTTP-заголовки и пути запросов. В проектах, использующих Retrofit, это может быть опасно, поскольку в таких строках часто содержатся URL-эндпоинты, токены доступа, ключи и другая конфиденциальная информация, которая хранится в APK-файле в открытом виде.
Рисунок 3. Фрагмент декомпилированного кода после применения ProGuard/R8 (без протектора)
Протектор дополнительно обфусцирует именно эти значения, закрывая этот вектор атаки.
Рисунок 4. Фрагмент декомпилированного кода после применения ProGuard/R8 + протектора
3. Строковые идентификаторы ресурсов и их значения
После применения только ProGuard/R8 ресурсы остаются в APK-файле в открытом виде. Например, в res/values/strings.xml по-прежнему можно увидеть значение google_api_key.
Рисунок 5. Значение google_api_key хранится в открытом виде до применения протектора
ProGuard/R8 здесь ничего не скрывает. Такой APK-файл декомпилируется, и ключ легко извлекается. Технически это опасно, потому что ключ становится доступным любому, кто скачал приложение. С его помощью можно исчерпать баланс средств для доступа к API (если неправильно настроено), и отправить много фейковых запросов в систему аналитики, чтобы исказить ее и усложнить принятие продуктовых решений. Ключ google_api_key — это лишь пример: на видном месте могут оказаться в принципе любые забытые по ошибке разработчика секреты. При использовании протектора такие строки шифруются и их извлечение становится почти невозможным.
Рисунок 6. Обфусцированное значение google_api_key после применения протектора
Примеры выше наглядно показывают разницу ролей: ProGuard/R8 делает код компактнее, а протектор закрывает его от реверса.
В комбинированном подходе важно не просто использовать ProGuard/R8 и протектор, а строго соблюдать порядок их выполнения. От этого напрямую зависит, будет ли стектрейс читаемым в системах сбора крашей. При нажатии на Build в Android Studio порядок задач сборки должен быть таким:
Листинг 3. Правильная цепочка задач сборки
1. Task :app:minifyReleaseWithR8 # базовая минимизация и первый mapping-файл 2. Task :app:obfuscateApkWithMazeRelease # применение протектора 3. Task :app:uploadCrashlyticsMappingFileRelease # загрузка финального mapping-файла в выбранную систему краш-аналитики
Такой порядок гарантирует, что стектрейсы будут корректно восстановлены и соответствовать защищённому APK-файлу. Если нарушить эту цепочку, то стектрейсы перестанут корректно восстанавливаться.
Рисунок 7. Фрагмент восстановленного стектрейса в RuStore Tracer после применения ProGuard/R8 + протектора
Достоинства подхода:
Недостатки подхода:
В результате такой подход дает максимальную защиту сборки для продакшена. Если разработчик заинтересован в продвинутой защите APK-файла от реверса, то комбинированный вариант является оптимальным выбором.
Универсального решения для всех проектов не существует. Выбор зависит от задач приложения, требований к безопасности и допустимого уровня риска.
ProGuard/R8 — это про скорость. Он ускоряет разработку: пайплайны проще, уже встроен в Android Studio, по умолчанию интегрируется с системами краш-аналитики. Этот подход стоит использовать на стадиях прототипа и MVP, когда про безопасность думать еще преждевременно. Но он не подходит как инструмент защиты от реверса.
ProGuard/R8 + протектор — это следующий уровень, и это уже про настоящую защиту. Он закрывает не только имена классов, но и строки, аннотации, ресурсы и бизнес-логику. Такой подход усложняет реверс и делает атаку заметно дороже.
Именно поэтому в продакшене целесообразно ориентироваться на второй вариант, так как в условиях публичного распространения приложения одного ProGuard/R8 недостаточно.
При этом важно применять современные протекторы, которые:
В результате защита не мешает разбору инцидентов. Разработчик получает защищенное приложение и читаемый стектрейс.
Теперь вы знаете, как подружить крашлитику и обфускацию. А если хотите повысить защиту мобильного приложения в кратчайшие сроки, не прилагая усилий, то можете воспользоваться уже готовым Gradle-плагином PT MAZE.