TL; DR Я переписал байт-код, чтобы разблокировать премиум-функции приложения на моем устройстве, и теперь я покажу вам, как предотвратить это в вашем приложении.
Платные функции с биллингом
Многие приложения предлагают пользователям совершать покупки внутри приложений — премиум-функции, какие-то виртуальные монеты для игр и т.д. После оплаты с помощью учетной записи Google покупки навсегда сохраняются на сервере Google с уникальным идентификатором. Это позволяет использовать купленные предметы на всех устройствах.
Но как, например, насчет Huawei, где сервисы Google Play не работают/недоступны? Huawei предоставляет почти идентичный API покупок в приложениях, хотя пользователь не может переносить свои покупки и подписки между платформами.
Подробнее о реализации можно прочитать здесь: https://developer.android.com/google/play/billing/integrate.
Проблема
Иногда приложения пытаются использовать более простые пути и не следуют инструкциям и рекомендациям Google. Это означает, что иногда они отказываются от проверки ранее приобретенных элементов при запуске приложения, тем самым оставляя для любого злоумышленника дверь открытой. Делать что-то на устройстве — всегда плохая идея.
Приведенный ниже рассказ применим к широкому кругу приложений и такого рода внедрения могут выполняться на разных уровнях. В этой статье делается попытка подчеркнуть очевидные болевые точки и рекомендовать решения.
Давайте поговорим о моем варианте: я купил премиум-функцию в Google Play, но само приложение не было доступно в магазине Huawei. Поэтому я скачал APK и установил его вручную, но премиум-функции были недоступны.
Статический анализ кода
Самый первый шаг — декомпилировать APK в код Smali. В ходе анализа я обнаружил, что это приложение X сохраняет логический флаг в Android SharedPreferences после успешной транзакции. Выполнение таких операций на устройствах — недопустимо.
Этот логический флаг использовался, чтобы проверить позже в приложении, куплена ли у пользователя расширенная функция.
В результате мы можем найти правильное место для вставки нашего байт-кода, чтобы обойти проверку — навсегда заменить ее флагом TRUE.
Smali
Я не хочу сразу переходить к результату и подробно объяснять, что происходит. Начну издалека. Когда мы компилируем приложение Java, создается байт-код. На платформе Android у нас раньше была виртуальная машина Dalvik, а теперь — Android Runtime (ART). Вместо байт-кода Java он преобразует исходный код в набор инструкций в формате .dex, который можно преобразовать в читаемый человеком код Smali. Smali очень похож на байт-код Java.
Вот фрагмент начала метода onCreate из MainActivity в Smali:
Его можно перевести в:
где константа flag 128 означает FLAG_KEEP_SCREEN_ON из WindowManager.
Самая первая строка — это сигнатура метода, но что это за .locals, p0, p1, v0?
- v0..v15 — это просто локальные переменные. У методов 16 регистров.
- .locals с числом описывают, сколько свободных переменных имеет этот метод. Это чисто отладочный флаг, установленный декомпилятором, который нам очень помогает при статическом анализе кода.
- p0..p15 — параметры метода, который мы вводим.
Например, onCreate имеет 2 параметра p0 и p1, которые переводятся как «this» и Bundle соответственно -> super.onCreate(Bundle)
Практическое правило: повторно используйте существующие переменные «v», когда это возможно.
Коды операций Dalvik вы можете найти тут.
Внедрение кода
После всех вышеперечисленных шагов мы знаем, что для обхода проверки наличия премиум-функции нам просто нужно ввести код, который устанавливает логическое значение до того, как что-либо загрузится. Наиболее предпочтительное место находится в классе Application или в основной Activity (зависит от приложения). Допустим, мы собираемся внедрить его в MainActivity#onCreate.
Этот фрагмент кода такой же, как
SharedPreferences.Editor editor = sharedPref.edit(); edit.putBoolean("preferencePremium", true); edit.apply();
Самый простой способ добиться этого — написать реальный код Kotlin/Java, скомпилировать и затем декомпилировать его, чтобы извлечь из него Smali, или напрямую написать байт-код Smali, который я предпочитаю для таких простых задач.
После этого приложение всегда будет запускаться с логическим флагом true.
Тот же метод используют сайты, распространяющие взломанные приложения.
Как предотвратить?
- Перенесите логику в свой бэкенд
- Всегда проверяйте купленный предмет, а затем предоставляйте доступ к приложению
- Делайте частые проверки покупок в Google через свой бэкенд
- Используйте шифрование для локального сохранения данных и желательно использовать ключ шифрования для конкретного устройства
- Используйте SafetyNet для отключения устройств с root-доступом и потенциального использования Xposed Framework
Проверьте, какие приобретенные элементы доступны для текущего пользователя при запуске приложения, а затем продолжайте работу. Не храните на устройстве ничего важного. Если вы собираетесь сохранить разблокированный контент на устройстве, не забудьте часто проверять его действительность и зашифровать его с помощью ключа шифрования для конкретного устройства. В случае злонамеренных действий рекомендуется использовать временный бан.
Прочтите еще рекомендации от Google: https://developer.android.com/google/play/billing/security
На этом пока все.
Спасибо!