Skip to content

Latest commit

 

History

History
409 lines (312 loc) · 37.6 KB

File metadata and controls

409 lines (312 loc) · 37.6 KB

Безопасность мобильного приложения: теория и ответы

Закрывает блок 12 чеклиста, в банке вопросов — блок 11 (вопросы 143–150).

Что уже разобрано в других материалах и здесь не дублируется:

  • экспортируемые компоненты, android:exported, deep links и App Links как механика — 10-android-sdk-deep.md, разделы 1 и 5.4;
  • разрешения и хранилища — там же, разделы 9 и 10;
  • R8 как инструмент сборки и его правила — 13-di-build-deep.md, раздел 10.

Здесь — модель угроз и то, что из неё следует: где проходит граница доверия, как хранить секреты, что делать с сетью, и почему половина «защитных» мер на клиенте не защищает.


1. Главный принцип: клиент не является границей доверия

1.1. Почему это первый вопрос блока

Вопрос 143 банка проверяет базовое понимание, без которого остальные ответы не имеют смысла. Формулировка, которую стоит выучить:

Приложение целиком находится в руках потенциального злоумышленника. Он может декомпилировать APK, подменить его, запустить на рутованном устройстве, подцепить отладчик, перехватить и изменить трафик. Значит, любая проверка на клиенте — это UX, а не безопасность. Единственная граница доверия — сервер.

Из этого следует конкретика, которую стоит привести примером, а не абстракцией:

  • Проверка «пользователь премиум» на клиенте определяет, показать ли кнопку. Право выполнить платную операцию проверяет сервер, при каждом запросе.
  • Валидация формы на клиенте существует, чтобы не гонять сеть зря. Сервер валидирует заново и никогда не доверяет присланному.
  • Скидка, цена, лимит, роль — вычисляются на сервере. Клиент их только отображает.

1.2. Что тогда вообще делает клиентская защита

Раз клиент не граница доверия, зачем пиннинг, обфускация и root-детект? Правильный ответ — повышение стоимости атаки и сокращение массовости:

  • Массовая атака (скрипт, который работает у тысяч людей) становится дороже и требует квалификации. Это отсекает большинство.
  • Целевая атака на конкретного мотивированного исследователя не отсекается ничем. Исходить надо из того, что она удастся.

Поэтому здоровая позиция: клиентские меры — это про экономику атаки и телеметрию, а не про гарантии. Кандидат, который обещает «защитить приложение от реверса», сигнализирует о непонимании; кандидат, который говорит «сделаю невыгодным и узнаю, что пытались», — о понимании.


2. Сеть

2.1. Базовый уровень

TLS обязателен везде; открытый HTTP запрещён (с Android 9 это и так дефолт — cleartext выключен, и включать его исключениями стоит только для локальной разработки). Конфигурация задаётся через network security config, а не кодом: это декларативно, видно на ревью и не размазано.

Что стоит держать в конфиге:

  • запрет cleartext для продовых доменов;
  • отдельный debug-overrides для дебажных сборок — там можно доверять пользовательским сертификатам, чтобы работал прокси-снифер; в релизе этого быть не должно;
  • пины, если они используются.

Отдельно: по умолчанию приложения не доверяют пользовательским CA начиная с Android 7. Это и есть причина, по которой для отладки трафика нужен debug-overrides — и хороший ответ на вопрос «почему Charles не видит мой трафик».

2.2. Certificate pinning (вопрос 145)

Что пинить. Не сам сертификат, а SPKI-хеш публичного ключа (Subject Public Key Info). Причина: сертификат перевыпускается регулярно (у Let's Encrypt — каждые 90 дней), и пин по сертификату сломает приложение при первом же перевыпуске. Публичный ключ при перевыпуске может сохраняться.

Почему не leaf, а промежуточный. Пин на конечный сертификат означает, что любая ротация ключа ломает всех, кто не обновился. Пин на промежуточный сертификат CA переживает ротацию листа и при этом всё ещё сильно сужает множество допустимых сертификатов. Это стандартный компромисс между безопасностью и эксплуатационным риском.

Backup-пин обязателен. Всегда пинится минимум два ключа: текущий и запасной, который ещё не используется. Без запасного любая внеплановая смена ключа (в том числе компрометация) означает, что все установленные приложения перестают работать до релиза — а релиз через Play идёт днями.

Срок годности. В network security config у пинов есть expiration. После этой даты пиннинг перестаёт применяться. Это выглядит контринтуитивно («защита сама отключается»), но это защита от кирпича: лучше приложение без пиннинга, чем неработающее приложение у пользователей, которые не обновлялись год.

Follow-up: «пиннинг обошли через Frida, что дальше». Ожидаемый ответ — не «усилим пиннинг». Правильно:

  1. Признать, что на скомпрометированном устройстве пиннинг не спасает by design: Frida работает внутри процесса и просто подменяет проверку.
  2. Перенести защиту туда, где она работает: подпись запросов, привязка сессии к устройству, аттестация через Play Integrity, серверные лимиты и аномалия-детекция.
  3. Использовать факт обхода как сигнал: аномальный трафик, невалидные вердикты целостности, всплеск операций с одного аккаунта.

2.3. Что ещё относится к сети

  • Не логировать тела запросов в релизе. Логирующий интерцептор OkHttp с уровнем BODY в проде — это токены и персональные данные в logcat, доступные при отладке и в баг-репортах. Уровень логирования должен зависеть от типа сборки.
  • Не класть секреты в приложение. Ключ API, зашитый в код, извлекается из APK за минуты, и BuildConfig, NDK или обфускация этого не меняют — они только меняют время. Если ключ нужен для стороннего сервиса, правильная схема — проксировать через свой бэкенд.

3. Хранение данных на устройстве

3.1. Модель угроз

Прежде чем выбирать API, надо назвать, от чего защищаемся, — иначе ответ будет про инструменты, а не про риск.

Угроза Насколько реальна Что помогает
Другое приложение читает наши файлы низкая: sandbox изолирует ничего дополнительного не нужно
Физический доступ к разблокированному устройству средняя биометрия на вход, FLAG_SECURE
Физический доступ к выключенному устройству низкая: FBE шифрует диск шифрование ОС уже работает
Рутованное устройство / вредонос с root высокая для целевых атак ничего не помогает полностью
Утечка через бэкап средняя исключить секреты из бэкапа
Утечка через логи и крэш-репорты высокая, самая частая дисциплина логирования

Главный вывод, который стоит произнести: на современном Android весь пользовательский раздел уже зашифрован (file-based encryption), и приложения изолированы. Поэтому дополнительное шифрование защищает в основном от сценария «root» — где оно, строго говоря, тоже пробивается. Это не значит «не шифровать»; это значит «не считать шифрование на устройстве главной мерой».

3.2. Важное изменение: EncryptedSharedPreferences больше не рекомендуется

Факт, который в 2026 году отличает актуальные знания от устаревших: библиотека androidx.security:security-crypto, из которой брали EncryptedSharedPreferences и EncryptedFile, объявлена устаревшей целиком. Причём необычным образом: депрекация приехала ещё в альфа-версиях, а затем вышел стабильный релиз, в котором все API уже помечены устаревшими. То есть это не «скоро уберут» — это официальный отказ от подхода.

Официальные замены выглядят обескураживающе прямолинейно: вместо EncryptedSharedPreferences предлагается обычный SharedPreferences, вместо EncryptedFile — обычный File, а вместо MasterKey — самостоятельная работа с KeyGenerator и Android Keystore. Логика Google в том, что диск и так зашифрован средствами ОС (см. модель угроз выше), а тем, кому нужно именно прикладное шифрование, следует делать его явно через Keystore, а не через обёртку.

Практичный современный ответ на «где хранить токен»:

  • DataStore плюс собственное шифрование через Keystore, если шифрование действительно нужно. Появился и первый официальный кирпич для этого — сериализатор DataStore, шифрующий содержимое (пока в альфа-версии), так что схема перестаёт быть полностью самодельной.
  • Механизм платформы для токенов, переживающих переустановку и перенос устройства, если задача именно в этом: Google предоставляет отдельное хранилище для небольших секретов с сквозным шифрованием, специально предназначенное для токенов аутентификации.
  • Ничего не хранить там, где это возможно, — см. 3.4.

Если на интервью вы упомянете EncryptedSharedPreferences как рекомендуемый подход, это считывается как знание образца 2023 года. Достаточно добавить полфразы: «раньше брали EncryptedSharedPreferences, но библиотека задепрекейчена, и сейчас это Keystore напрямую».

3.3. Android Keystore (вопрос 144)

Единственный по-настоящему сильный механизм на устройстве. Ключевое свойство: приватный ключ не покидает защищённое хранилище и в идеале живёт в отдельном аппаратном модуле. Приложение работает с ключом по ссылке — просит подписать или расшифровать, но не может извлечь материал.

Что это даёт и чего не даёт:

  • Даёт: ключ нельзя скопировать с устройства даже с root. Украсть зашифрованные данные можно, расшифровать их на другом устройстве — нет.
  • Не даёт: на скомпрометированном устройстве атакующий может воспользоваться ключом, просто попросив Keystore о том же, о чём просит приложение.

Практические свойства, о которых стоит знать:

  • Аппаратная привязка. Ключи могут храниться в TEE или в отдельном чипе (StrongBox). Наличие StrongBox проверяется, и при его отсутствии нужен фолбэк — иначе приложение упадёт на устройствах без него.
  • setUserAuthenticationRequired(true) делает ключ доступным только после аутентификации пользователя. Можно задать окно валидности в секундах либо потребовать аутентификацию на каждое использование (тогда ключ используется через CryptoObject в BiometricPrompt). Это то, что превращает биометрию из «проверки в UI» в реальную криптографическую гарантию: без успешной аутентификации данные не расшифровать в принципе.
  • Инвалидация при смене биометрии. Если задать соответствующий флаг, ключ автоматически становится непригодным при добавлении нового отпечатка. Это защита от сценария «злоумышленник добавил свой палец». Обратная сторона — легальная смена биометрии тоже ломает ключ, и это надо обработать (перевыпуск ключа и повторный вход), иначе получите поток жалоб.
  • Key attestation — способ доказать серверу, что ключ действительно порождён в аппаратном хранилище, а не подсунут. Устройство выдаёт цепочку сертификатов, которую проверяет бэкенд (на клиенте это бессмысленно). Деталь, актуальная именно сейчас: корней доверия два — исторический и новый, — и проверяющая сторона обязана принимать оба, потому что старые устройства на новый корень никогда не перейдут. Отдельно существует механизм удалённой выдачи ключей, обязательный на устройствах, вышедших с последними версиями Android.

3.4. Что хранить, а что не хранить

Самая сильная стратегия — не хранить:

  • Пароль пользователя не хранится никогда, ни в каком виде.
  • Долгоживущий refresh-токен — минимально необходимый срок, с ротацией при каждом использовании, и с возможностью отзыва на сервере.
  • Access-токен короткоживущий, и его утечка ограничена по времени по построению.
  • Данные карт не хранятся вообще (раздел 6).

Что нужно исключить из бэкапа: токены, ключи, всё, что привязано к устройству. Иначе секрет уезжает в облако и приезжает на другое устройство — а autoBackup включён по умолчанию. Для этого есть правила извлечения данных; их надо писать явно, а не полагаться на дефолт.


4. Подтверждение подлинности клиента

4.1. Play Integrity (вопрос 146)

Механизм, который отвечает на вопрос «это действительно наше приложение на настоящем Android?». Приложение запрашивает токен, отправляет его на свой бэкенд, бэкенд обращается к Google за расшифровкой и получает вердикты по нескольким осям: соответствует ли приложение тому, что опубликовано в Play (не пересобрано и не подменено), похоже ли устройство на настоящее и прошедшее сертификацию, лицензирован ли аккаунт.

Три вещи, за которые ставят плюс:

  • Проверка обязана быть на сервере. Если приложение само разбирает вердикт и решает, что делать, механизм бесполезен — эту проверку патчат первой. Токен идёт на бэкенд, решение принимает бэкенд.
  • requestHash привязывает токен к конкретному действию. В запрос кладётся хеш содержимого операции (например, суммы перевода). Без этого токен можно переиспользовать для другой операции — классическая replay-атака.
  • Вердикт не булев. Это набор сигналов, и реакция должна быть градуированной, а не «пускать / не пускать» (раздел 4.3).

Ограничения, которые стоит назвать честно: есть квоты на запросы, поэтому дёргать на каждое действие нельзя; сервис требует наличия Play Services, поэтому на устройствах без Google (в том числе значимая часть рынка) он просто не работает, и нужен запасной путь.

4.2. Root-детект (вопрос 147)

Ключевой тезис: это телеметрия, а не гейт.

Почему не гейт:

  • Надёжного детекта не существует. Любая проверка — это поиск известных признаков, а сокрытие root развивается быстрее детекта.
  • Ложные срабатывания бьют по легальным пользователям: кастомные прошивки, разработчики, корпоративные устройства.
  • Блокировка рутованных устройств отсекает лояльных технарей и не отсекает атакующего.

Как правильно: собирать сигналы (признаки root, эмулятора, отладчика, инструментирования, подозрительных модулей) и повышать требования пропорционально риску операции:

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

Такая формулировка показывает продуктовое мышление: безопасность соразмерна риску, а не одинакова везде.

4.3. Обфускация R8 (вопрос 148)

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

От чего не защищает:

  • Строки остаются строками. URL, ключи и сообщения видны в дампе ресурсов.
  • Логика остаётся логикой. Тот, кто ищет конкретную проверку, найдёт её по строкам, по вызовам системных API или динамически, под отладчиком.
  • Публичные точки входа не переименовываются: имена Activity, сервисов, всё, что упомянуто в манифесте и в рефлексии, остаётся.
  • От модификации не защищает вообще: приложение пересобирается и подписывается заново.

Практическая связка, которую стоит добавить: обфускация ломает стектрейсы, поэтому обязателен mapping.txt и его загрузка в систему крэш-репортинга. Команда, которая включила R8 и потеряла читаемость крэшей, — типичная и болезненная история.


5. Аутентификация пользователя

5.1. Биометрия

BiometricPrompt — единственный правильный способ: он показывает системный диалог, единообразно работает с отпечатком, лицом и фолбэком на PIN/пароль устройства, и учитывает различия вендоров. Самому рисовать экран биометрии нельзя.

Что отличает поверхностный ответ от глубокого:

  • Классы стойкости. Биометрия делится на сильную и слабую; криптографические операции (CryptoObject) доступны только для сильной. Распознавание лица на многих устройствах относится к слабой — значит, «войти по лицу» может быть доступно, а «расшифровать ключ по лицу» — нет.
  • Биометрия без криптографии — это UI. Если результат проверки — просто true в колбэке, её обходят патчем. Реальная гарантия появляется, когда успешная аутентификация разблокирует ключ в Keystore, которым расшифровываются данные.
  • Фолбэк обязателен. Биометрия может быть не настроена, временно заблокирована после неудачных попыток или недоступна на устройстве. Нужен путь через код-пароль.

5.2. Токены и сессии

Схема, которая считается правильной по умолчанию: короткоживущий access-токен, долгоживущий refresh-токен с ротацией, отзыв на сервере, привязка сессии к устройству.

Отдельно про OAuth в мобильном приложении: авторизация должна идти через системный браузер (Custom Tabs), а не через встроенный WebView. Причины: WebView контролируется приложением, то есть оно может прочитать введённые логин и пароль; пользователь не видит адресную строку и не может проверить домен; не работает существующая сессия браузера. Плюс PKCE обязателен, потому что мобильное приложение не может хранить client secret.


6. Платежи и чувствительный ввод (вопрос 149)

Главный принцип: не прикасаться к данным карты. Всё остальное — следствие.

  • Поля номера карты, срока и CVV отображает компонент платёжного провайдера (его SDK, его форма или его вебвью). Данные уходят напрямую провайдеру, минуя ваш код и ваш бэкенд. Приложение получает обратно токен и работает уже с ним.
  • Причина не в удобстве, а в сужении PCI DSS-скоупа: как только данные карты проходят через ваш код, в область сертификации попадает приложение, бэкенд, логи, инфраструктура и процессы команды. Это на порядок дороже, чем интеграция SDK. Именно эту связку и проверяет вопрос.
  • Что нельзя логировать: номер карты, CVV, полный трек, любые данные аутентификации. Никогда, ни на каком уровне, ни в аналитике, ни в крэш-репортах. CVV не хранится даже в зашифрованном виде — это прямо запрещено стандартом.
  • На таких экранах ставится FLAG_SECURE: он запрещает скриншоты и запись экрана и убирает содержимое из превью в списке недавних задач. Второе часто важнее первого.
  • Автозаполнение и клавиатурные подсказки на полях с чувствительными данными отключаются, чтобы значение не попало в системные словари и в сервисы автозаполнения.

Хороший финальный штрих в ответе: даже при полном использовании SDK провайдера остаётся ваша зона ответственности — целостность приложения (подмена SDK), защита от наложений поверх окна и корректная обработка результата платежа, включая идемпотентность (повтор не должен приводить ко второму списанию).


7. Поверхность атаки

Короткий чеклист, который закрывает 🟡-пункт чеклиста и вопрос 150:

  • Экспортируемые компоненты. Всё, что не предназначено для внешнего вызова, должно быть exported="false". Экспортированный компонент — это публичный API вашего приложения, который может вызвать любое приложение с любыми параметрами.
  • Валидация deep links. Приходящие ссылки — недоверенный ввод. Закрываемый класс уязвимостей: открытый редирект и подстановка чужого адреса (приложение получает ссылку с параметром ?next=https://evil.example, доверчиво по нему переходит и утекает токен либо показывает фишинговый экран внутри доверенного интерфейса). Лечение — allowlist доменов и схем, а не попытка отфильтровать плохое.
  • Проверка прав на входе, а не в UI. Компонент, который можно вызвать снаружи, обязан сам проверить, что вызывающий имеет право, — нельзя полагаться на то, что до него «дошли по правильному сценарию».
  • FLAG_SECURE на экранах с чувствительными данными.
  • Логи без персональных данных. Самая частая реальная утечка в мобильной разработке — не взлом, а логи, крэш-репорты и аналитика, куда положили лишнее. Инструмент — не только дисциплина, но и проверка: StrictMode, ревью схемы аналитики, автоматический поиск подозрительных полей.
  • Экраны поверх приложения. Наложение чужого окна поверх вашего (tapjacking) — реальный вектор на чувствительных подтверждениях; для критичных действий стоит игнорировать касания, когда окно перекрыто.

8. Приватность

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

  • Data Safety в Play. Декларация о том, какие данные собираются и куда уходят, обязательна, и она должна соответствовать действительности, включая сторонние SDK. Рекламный или аналитический SDK, который собирает больше заявленного, — это ответственность приложения.
  • Минимизация. Не собирать то, что не нужно продукту. Это одновременно самый дешёвый способ снизить риск: данных, которых нет, не бывает в утечке.
  • Локальное регулирование. GDPR в ЕС, 152-ФЗ в России (в том числе требование локализации персональных данных), отраслевые требования для финансов и здоровья. Знать детали наизусть не нужно, но нужно знать, что эти требования влияют на архитектуру (где физически лежат данные) и что решение принимается не разработчиком в одиночку.
  • Идентификаторы. Рекламный идентификатор сбрасывается пользователем и может быть удалён; привязывать к нему что-либо долгоживущее нельзя. Постоянных аппаратных идентификаторов на современном Android для приложений нет — и попытки собрать «стабильный отпечаток устройства» из доступных сигналов нарушают политику Play.

9. Чеклист самопроверки

  1. Почему клиент не является границей доверия и что из этого следует практически?
  2. Если клиентская защита ничего не гарантирует, зачем она нужна?
  3. Что настраивается через network security config и зачем нужен debug-overrides?
  4. Что именно пинят при certificate pinning и почему не листовой сертификат?
  5. Зачем backup-пин и почему у пиннинга есть срок годности?
  6. Пиннинг обошли через Frida. Что дальше?
  7. Модель угроз для данных на устройстве: от чего вы реально защищаетесь?
  8. Что случилось с EncryptedSharedPreferences и чем это заменяют сегодня?
  9. Что даёт Keystore и чего он не даёт на скомпрометированном устройстве?
  10. Что делает setUserAuthenticationRequired и в чём подвох инвалидации по биометрии?
  11. Что такое key attestation, кто её проверяет и почему корней доверия два?
  12. Play Integrity: где обязана происходить проверка и зачем requestHash?
  13. Почему root-детект — телеметрия, а не гейт? Как выглядит градуированная реакция?
  14. От чего защищает и от чего не защищает обфускация R8?
  15. Почему биометрия без криптографии — это UI, а не защита?
  16. Почему OAuth должен идти через системный браузер, а не через WebView?
  17. Как сузить PCI-скоуп при вводе карты и что нельзя логировать никогда?
  18. Какой класс уязвимостей закрывает allowlist для deep links?
  19. Где происходит самая частая реальная утечка данных в мобильной разработке?
  20. Что такое Data Safety и почему сторонние SDK — ваша ответственность?