Crashlytics против Обфускации в Android: как усложнить взлом, а не разработку

Crashlytics против Обфускации в Android: как усложнить взлом, а не разработку
image
Автор - Александр Ананикян, аналитик PT MAZE

Краши в мобильных приложениях — это нормальная часть жизни продукта. Разработчики тратят много времени на то, чтобы находить причину падения приложения и исправлять её. Параллельно есть и другая задача: сделать код безопасным, чтобы усложнить взлом приложения.

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

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

На практике сейчас существуют два способа переименовывать код.

1. ProGuard/R8 (без протектора).

Это класс инструментов оптимизации и переименования, который выполняется при стандартной сборке Android-проекта через Gradle-плагин. Он минимизирует код, генерирует mapping.txt и корректно работает с системами crash-аналитики.

2. ProGuard/R8 + протектор.

Это двухслойная защита: сначала ProGuard/R8 выполняет минимизацию и переименование, затем протектор обфусцирует байт-код и ресурсы приложения, обновляет существующий mapping-файл и возвращает его обратно.

И здесь возникает еще одна проблема: не все системы сбора крашей одинаково хорошо обрабатывают mapping-файлы после применения серьезных техник защиты. Сейчас чаще всего используются такие сервисы:

И если mapping.txt обработан некорректно, то эти системы не смогут восстановить стектрейс, и разработчик не увидит реальные места падения приложения.

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

2. ProGuard/R8

При использовании только 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:

  • переименовывает классы, методы и аннотации (@GET, @POST, @Headers);
  • сокращает имена сущностей до коротких идентификаторов;
  • удаляет неиспользуемый код;
  • может встраивать (inline) вызовы.

Но при этом ProGuard/R8 не обрабатывает строки и ресурсы.

В открытом виде остаются:

  • строки в байт-коде;
  • строковые значения в параметрах аннотаций;
  • строковые идентификаторы;
  • значения ресурсов.

Если значимая часть логики лежит в ресурсах, строках, то ProGuard/R8 её не скрывает. Эти значения дают достаточно подсказок современным декомпиляторам, и все важные данные сразу становятся видны хакеру.

Достоинства подхода:

  • высокая скорость сборки;
  • подходит для тестирования и внутренних сборок;
  • больше контроля при интеграции crash-аналитики.

Недостатки подхода:

  • слабая защита;
  • строки, ресурсы и ассеты остаются открытыми;
  • легко поддается реверсу.

Вывод по ProGuard/R8

ProGuard/R8 — это инструмент оптимизации и удобной сборки, а не инструмент защиты. Он ускоряет разработку и процесс доставки приложения пользователям, упрощает отладку. Это подходит, если вам пока рано думать о безопасности, и вы активно заняты развитием продукта. Но если нужна защита от реверса, то одного ProGuard/R8 мало.

3. ProGuard/R8 + протектор

При применении комбинированного подхода чтение стектрейса не вызывает проблем, при условии, что протектор корректно обрабатывает mapping.txt после применения своих техник запутывания и упаковки кода. Здесь важный момент: протектор не создаёт новый mapping-файл, а берёт существующий файл от ProGuard/R8, модифицирует байт-код и ресурсы приложения, исправляет mapping-файл и возвращает его обратно. В результате даже сильная обфускация не мешает диагностике ошибок.

Что добавляет протектор поверх ProGuard/R8

Современные протекторы меняет не только имена классов, методов и полей, но и те элементы байт-кода, которые 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 + протектора

Достоинства подхода:

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

Недостатки подхода:

  • продакшен-сборка занимает больше времени;
  • нужно строго следить за порядком задач;
  • возрастает риск перепутать версии сборки и потерять читаемый стектрейс.

Вывод по ProGuard/R8 + протектор

В результате такой подход дает максимальную защиту сборки для продакшена. Если разработчик заинтересован в продвинутой защите APK-файла от реверса, то комбинированный вариант является оптимальным выбором.

4. Коротко: что выбрать

Универсального решения для всех проектов не существует. Выбор зависит от задач приложения, требований к безопасности и допустимого уровня риска.

  1. ProGuard/R8 — это про скорость. Он ускоряет разработку: пайплайны проще, уже встроен в Android Studio, по умолчанию интегрируется с системами краш-аналитики. Этот подход стоит использовать на стадиях прототипа и MVP, когда про безопасность думать еще преждевременно. Но он не подходит как инструмент защиты от реверса.

  2. ProGuard/R8 + протектор — это следующий уровень, и это уже про настоящую защиту. Он закрывает не только имена классов, но и строки, аннотации, ресурсы и бизнес-логику. Такой подход усложняет реверс и делает атаку заметно дороже.

Именно поэтому в продакшене целесообразно ориентироваться на второй вариант, так как в условиях публичного распространения приложения одного ProGuard/R8 недостаточно.

При этом важно применять современные протекторы, которые:

  • корректно обрабатывают mapping-файл после всех этапов защиты;
  • автоматически передают финальный mapping-файл в системы краш-аналитики;
  • не создают дополнительную нагрузку на разработку.

В результате защита не мешает разбору инцидентов. Разработчик получает защищенное приложение и читаемый стектрейс.

Теперь вы знаете, как подружить крашлитику и обфускацию. А если хотите повысить защиту мобильного приложения в кратчайшие сроки, не прилагая усилий, то можете воспользоваться уже готовым Gradle-плагином PT MAZE.

Вход по SSH

Проникаем за кулисы сложных атак и показываем то, что обычно скрыто за консолью и логами.

Получи root-доступ!