Отсутствие проверки целостности приложения
![]() |
Критичность: СРЕДНЯЯ |
| Способ обнаружения: IAST |
Описание
Одним из векторов атаки на мобильные приложения является так называемый code tampering — изменение кода приложения. Злоумышленники могут изменять код для получения преимуществ в ходе работы приложения, включения платных возможностей, отключения рекламы и различных проверок, распространения вредоносного кода вместе с приложением через альтернативные площадки дистрибуции.
Приложение считается уязвимым, если во время работы оно никак не проверяет собственную целостность — например, не сверяет сертификат, которым подписана текущая установка, с эталонным. В этом случае атакующий может декомпилировать APK, внести изменения (отключить проверки, добавить вредоносный код), переподписать своим ключом и распространить модифицированную сборку, а приложение не заметит подмены.
Проблема
Отсутствие проверки целостности приводит к следующим рискам:
- Обход клиентской логики и платных функций. Атакующий правит байткод и снимает проверки лицензий, подписки, рекламы, флагов feature-toggle.
- Внедрение вредоносного кода. Перепакованное приложение со встроенным шпионским/мошенническим кодом распространяется через сторонние магазины и фишинговые сайты под видом оригинала.
- Компрометация защитных механизмов. Снятие или подмена проверок на root, отладчик, SSL Pinning открывает путь к перехвату трафика и краже данных.
- Репутационный и финансовый ущерб. Модифицированные сборки наносят ущерб пользователям и бренду владельца приложения.
Информация
Проверка подписи на устройстве — это мера, повышающая стоимость атаки, а не абсолютная защита: решительный атакующий может найти и нейтрализовать саму проверку. Эффективность достигается многоступенчатой реализацией (в том числе в нативном коде) и серверной верификацией. Для комплексной оценки доверенности среды используйте Play Integrity API.
Рекомендации
Чтобы усложнить модификацию кода приложения, разработчики могут воспользоваться механизмом проверки подписи приложения во время его работы. С помощью объекта класса PackageManager можно получить хеш сертификата, которым было подписано приложение, и сравнить его с эталонным проверочным значением. Если значения совпадают, приложение не было переподписано.
Используйте актуальные API в зависимости от версии ОС:
- API 28+ (Android 9): метод
PackageManager.hasSigningCertificate()— напрямую сверяет хеш/сертификат подписи с эталонным байтовым массивом (самый удобный способ). - Получение списка сертификатов: флаг
PackageManager.GET_SIGNING_CERTIFICATES(API 28+) и полеPackageInfo.signingInfo.
Устаревший API: GET_SIGNATURES / PackageInfo.signatures
Флаг PackageManager.GET_SIGNATURES и поле PackageInfo.signatures помечены как deprecated с API 28. Кроме того, GET_SIGNATURES исторически уязвим к атаке Janus/подмене и не учитывает ротацию ключей (signing key rotation, APK Signature Scheme v3). На API 28+ используйте GET_SIGNING_CERTIFICATES/signingInfo; обращение к GET_SIGNATURES допустимо только как fallback для API ниже 28.
Пример кода проверки:
public boolean checkSign(String crt) {
PackageManager pm = getPackageManager();
String sign = crt.replace(":", "");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// API 28+: актуальный способ, корректно работает в том числе при ротации ключей
return pm.hasSigningCertificate(getPackageName(), hexToByte(sign), PackageManager.CERT_INPUT_SHA256);
} else {
try {
// Fallback для API < 28: GET_SIGNATURES помечен deprecated, использовать только здесь
@SuppressWarnings("deprecation")
Signature signature = pm.getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES).signatures[0];
byte[] pkgSign = MessageDigest.getInstance("SHA-256").digest(signature.toByteArray());
return Arrays.equals(hexToByte(sign), pkgSign);
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
return false;
}
Для получения хеша сертификата подписи релизной версии приложения можно использовать утилиту keytool:
keytool -list -v -keystore sign_key.jks -alias key0 -storepass 123456 -keypass 123456
здесь sign_key.jks — файл с ключом подписи, key0 — имя алиаса, storepass и keypass — пароли хранилища и ключа соответственно.
Некоторые замечания:
Не стоит делать единственный метод с названием checkSign() или подобным, так как злоумышленник все-равно будет менять код — ему ничего не стоит заставить данный метод возвращать нужное значение. Проверку подписи лучше всего организовать многоступенчато в разных местах приложения, в том числе и в нативном коде.
Хорошим способом защиты является проверка подписи на стороне сервера, при этом следует помнить, что сами запросы также легко подделать, поэтому необходимо продумать механизм аутентификации проверочных запросов к серверу.
