Поговорим про безопасность в Dart и Flutter

afc14fdd0edc159b9f773f8a20fc58aa.png

Безопасность приложения определяется всеми уровнями — от операционной системы и компилятора до используемых пакетов/плагинов и кода самого приложения. Особенно этот вопрос актуален, когда значительная часть используемых компонентов поддерживается сообществом и не контролируется единой организацией или фондом и, чем более популярной становится платформа, чем больше пакетов появляется и чем больше становится кодовая база платформа, тем больше вероятность возникновения уязвимостей разного уровня (особенно в низкоуровневом коде на C++, где возможна утечка памяти, переполнение буфера, состояние гонки и другие неприятности), а также внедрения вредоносного кода разработчиками из сообщества (начиная от безобидных баннеров, до внедрения шпионского кода, бэкдоров и деструктивных функций). В статье мы обсудим какие векторы атак возможны в Dart, какие меры предпринимает сообщество и Google для снижения рисков при создании платформы и пакетов (и про бейджик openssf), и как можно обезопасить себя.

Начнем с самого низкого уровня и посмотрим прежде всего на компилированный код, который может запускаться как независимый исполняемый образ (при использовании AOT-компиляции), либо передаваться на исполнение виртуальной машине (в JIT, Kernel или Dill-образах). В первом случае создается код, скомпилированный из нашего приложения и исходных текстов используемых пакетов/плагинов, собранный с компонентами платформы (только используемые функции после tree-shaking) и нативным кодом плагинов. Во втором случае исполнение происходит внутри среды исполнения, при этом код представлен в виде операторов и их параметров (своеобразного байт-кода), в большей или меньшей степени напоминающих исходный код приложения. Потенциальные уязвимости могут быть в реализации используемых пакетов из Dart SDK (ссылка на runtime, реализован на C++) и обновляемый список уязвимостей Dart SDK можно посмотреть здесь или здесь (при этом атак, нацеленных на повышение привилегий в них сейчас нет). В целом приложение запускается в относительно изолированной среде выполнения (которую обеспечивает механизм изолятов) со своим управлением памятью и сборщиком мусора (здесь потенциальной угрозой может быть только переполнение памяти или запуск нагружающих процессор алгоритмов), но существует два сценария, когда приложение выходит за рамки сетевых взаимодействий и операций с файлами:

  • FFI (Foreign Function Interface) — вызов нативного кода (код будет скомпилирован и слинкован с приложением на этапе сборки);

  • использование Dart Embedding API, когда Dart-приложение встраивается во внешнее приложение и вызывается из него;

Поскольку внешний код никак не контролируется, он может вызвать проблемы с утечкой памяти, попытками выхода из userspace на уровень ядра системы, эксплуатацией уязвимостей процессора или видеоадаптера и прочие проблемы, характерные для нативных приложений. Особенную сложность представляет ситуация, когда внешний код запускает сетевой сервис (например, D-Bus клиент) и при неправильном использовании это может привести к эксплуатации уязвимости переполнения буфера и исполнения вредоносного кода. Непосредственно в Dart такой ситуации возникнуть не может, поскольку данные сохраняются в динамической памяти и отделены от исполняемого кода.

Но при этом важно помнить, что приложение выполняется в операционной системе от имени пользователя, который его запустил (или от владельца файла, если указан suid) и доступ к файлам и устройствам ввода-вывода, а также выделение памяти и процессорного времени регулируется механизмами операционной системы, поэтому (если не будет эксплуатироваться известная уязвимость системы или системного сервиса, что наиболее актуально для desktop-приложений) доступ к файлам будет ограничен правами пользователя (чаще всего модификация файлов запрещена везде, кроме домашнего каталога и каталога временных файлов). При этом dart: io представляет полный доступ к файлам на Desktop-системах и это потенциально может быть использовано во враждебных целях. На мобильных платформах доступ к чувствительной информации и сервисам регулируется разрешениями доступа.

Поскольку мы не контролируем весь Dart-код и подключаем к нашему приложению как минимум стандартную библиотеку, а в большинстве случаев еще и внешние пакеты или плагины (с платформенным кодом), то нужно быть уверенными, что автор пакета не добавит вредоносный код при обновлении. Самый простой способ избежать этой проблемы — указывать полную версию пакета (не использовать обобщения * или пустую строку, а также интервалы версий). При этом нужно отслеживать (вручную или автоматически) обнаружение уязвимостей в используемых пакетах и их зависимостях, чтобы своевременно установить обновление. Например, для этого может использоваться dependabot, который теперь поддерживает pub-репозитории (подробнее здесь).

Для анализа потенциально вредоносной активности после установки обновлений можно использовать несколько приемов:

  • перехват сетевого трафика и сравнение с эталонным — здесь может быть полезно использовать mitmproxy — свободный инструмент, который может не только перехватывать запросы и ответы (в том числе, по https с использование собственного сертификата), но и обрабатывать сессию с целью поиска неожиданных запросов (для этого можно использовать Python API);

  • перехват сообщений, которые отправляются в консоль — это можно осуществить через создание зоны с переопределенной функцией print, например runZoned(() { runApp(); }, zoneSpecification: ZoneSpecification(print: (Zone self, ZoneDelegate parent, Zone zone, String line) { parent.print(zone, "Intercepted: $line"); }));. В реализации print можно добавить проверку на наличие ключевых слов или обнаружение сообщений с необычного префикса (или без префикса);

  • отслеживание доступа к файлам — для этого может использоваться watcher (работает поверх inotify или собственных механизмов операционной системы), с помощью него можно обнаружить операции доступа к файлам в указанном местоположении (например, в каталоге данных приложения или в документах пользователя);

Также можно использовать инструменты статического анализа (они также позволяют обнаружить потенциальные уязвимости и плохие практики в собственном коде). Кроме встроенного анализатора, который отлично справляется с обнаружением неудачных фрагментов кода на Dart и Flutter, можно посмотреть в сторону Horusec — инструмента статического анализа, который (кроме множества других языков) поддерживает и Dart. Он может быть установлен локально (с веб-интерфейсом для просмотра обнаруженных уязвимостей) с Docker Compose или через Helm в кластер Kubernetes. Также его можно добавить в конвейер сборки, наряду с другими тестами.

Чтобы обезопасить свой продукт, нужно быть уверенным, что ни на одном этапе доставки кода риски внедрения вредоносного кода будут минимальными и весь процесс будет автоматизирован (чтобы исключить атаку supply chain, когда может быть внедрена уязвимость на любом уровне — начиная от внешних пакетов/плагинов и заканчивая процедурой доставки обновления до устройств пользователей). Это может быть достигнуто через ограничение возможности отправки изменений в мастер-ветку, peer-review, контроль версий устанавливаемых пакетов и др, и этот процесс тоже можно автоматизировать. Основные репозитории Dart и Flutter сейчас проходят автоматизированную процедуру проверки на соответствие требованиям безопасности OpenSSF (Open Source Security Foundation), результатом которого является отчет о возможных слабых местах в процессе управления зависимостями, оценки качества кода, управления ветками в системе контроля версий, сборки и публикации приложения. Инструмент анализа может быть запущен как Docker-контейнер (нужно передать Github-токен, поскольку забирается большое количество репозиториев для выполнения шагов анализа):

docker run -e GITHUB_AUTH_TOKEN=ghp_ gcr.io/openssf/scorecard:stable\
           --show-details --repo=https://github.com/flutter/flutter

Для проверки может использоваться любой репозиторий (--repo) или локальный каталог (--local). По умолчанию запускаются все проверки, в том числе на фиксирование версий зависимостей, наличие код-ревью, защита веток от отправки без ревью, отсутствие двоичных артефактов и другие, но список проверок может быть сокращен (--check).

Мы поговорили про возможные проблемы и способы их избежания для приложений на Dart, теперь перейдем к фреймворку Flutter. Кроме всего вышесказанного, нужно отметить еще несколько возможных областей нарушения безопасности, связанных с архитектурой фреймворка и особенностями хранения данных приложений при использовании SQLite и Shared Preferences (они не шифруются и могут быть прочитаны при наличии прав разработчика и root). Также при наличии root могут быть применены hook для переопределения реализации (например, так может быть обойдена проверка на наличие root-прав).

  • переопределение системных каналов — Flutter Engine регистрирует множество платформенных каналов для передачи сообщений и вызовов методов при изменениях в жизненном цикле приложения, событиях взаимодействия пользователя с экраном и устройствами ввода и других событиях платформы и определяет их по имени, на которые подписываются core-классы фреймворка. Здесь может быть потенциальная проблема в плагинах при переопределении источника платформенного канала или подписки на них для фиксации низкоуровневых событий (например, с целью отправки нежелательной аналитики), но простого способа обнаружить это без анализа исходных кодов нет, каналы не предоставляют необходимых методов для определения источника и других подписчиков (для EventChannel);

  • прямой доступ к объекту ui: window или использование Overlay для отображения элементов поверх экрана приложения (рекламы или баннеров);

Также для дополнительной защиты сгенерированного образа приложения можно использовать обфускацию (для усложнения декомпиляции кода, скрывает исходные названия классов, методов, переменных и затрудняет понимание алгоритма при дизассемблировании) через флаг --obfuscate в flutter build, а также хранить строковые ресурсы в отдельном файле (как asset) в зашифрованном виде (например, можно использовать библиотеку flutter_sodium).

Для обнаружения потенциальных проблем можно использовать следующие решения:

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

bool findWidget(Element element, Type T) {
  if (element.widget.runtimeType == T) return true;
  bool found = false;
  element.visitChildren((el) {
    if (findWidget(el, T)) found = true;
  });
  return found;
}

bool detectWidget(Type T) {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (findWidget(WidgetsBinding.instance.renderViewElement!, OverlayEntry)) {
      //сюда мы попадем, если на экране есть лишний OverlayEntry
    }
  });
}
  • обнаружение дополнительных разрешений (для Flutter-приложений), подозрительных фрагментов кода или наличия чувствительной информации — для этого можно использовать Mobile Security Framework, который работает с уже собранными приложениями (apk / ipa), обнаруживает трекеры (по наличию ссылок или фрагментов кода), а также определяет индекс безопасности приложения (с учетом разрешений, использования возможностей системы и других эвристик);

  • проверять приложение на соответствие исходному образу (например, использовать пакет flutter_security);

  • для избежания риска утечки данных можно проверять устройство на наличие взлома (jailbreak для iOS, root для android), можно использовать flutter_jailbreak_detection;

  • использовать in-app решения для анализа уязвимостей (например, freerasp от Talsec);

  • проверить на наличие небезопасного хранения чувствительной информации в своем коде или в коде пакетов (shared_preferences без шифрования для пин-кодов, токенов авторизации и др.)

  • проверить на использование SSL/TLS при взаимодействии с сервером (и на установку безопасных шифров и использование SSL Pinning для исключения подмены сертификата)

Мы рассмотрели несколько возможных угроз в Dart/Flutter приложении и инструменты, которые могут использоваться для их обнаружения и уменьшения вероятности попадания уязвимого кода в production. Конечно же, сейчас количество инструментов статического анализа не очень велико, но ситуация постепенно улучшается и безопасности открытого кода уделяется все больше внимания (например, в рамках работы Open Source Security Foundation). Приглашаю всех обсудить в комментариях, какие еще возможные векторы атак и уязвимости вы видите, и какие инструменты были бы полезны для сохранения Flutter-экосистемы и приложений безопасными.

В новой версии Flutter 3.0 была добавлена официальная поддержка игровых движков Flare и SpriteWidget и набор инструментов CasualGamingKit. Хочу пригласить вас на бесплатный урок, где мы изучим возможности Flutter для создания кроссплатформенных игр для мобильных устройств, веб и настольных компьютеров и создадим простую аркадную игру от начала и до подготовки к публикации в магазинах приложений.

© Habrahabr.ru