Эволюция уязвимостей в приложениях для Android - (не)Уникальный опыт

Эволюция уязвимостей в приложениях для Android


История развития приложений для Android прошла несколько заметных этапов: небольшие приложения, работающие локально, клиент-серверные приложения, экосистемы приложений и суперапы (super-app). Каждый из этих этапов повышал планку сложности, порождал новые уязвимости и заставлял разработчиков все больше заботиться о безопасности как самих приложений, так и данных, которыми они оперируют. Развивалась и сама операционная система, предоставляя разработчикам больше возможностей и механизмов обеспечения безопасности. Но в этой системе уравнений всегда несколько больше неизвестных, чем кажется на первый взгляд. Эта статья о том, как эволюционировали уязвимости мобильных приложений, что на это влияло, какие уязвимости актуальны сейчас и какие ждут нас в будущем.

Основные уязвимости приложений для Android

Существует довольно много видов уязвимостей мобильных приложений, но среди них можно выделить некоторые обобщенные типы, которые покрывают основной ландшафт. Самые популярные уязвимости связаны с небезопасным хранением пользовательских данных и данных приложения. Для их возникновения разработчику даже не нужно ничего делать — достаточно просто сохранять конфиденциальную информацию в незашифрованном виде. Некоторые разработчики, думая о безопасности, сохраняют такие данные во внутренней директории приложения — так называемой песочнице. Но в ряде случаев этого оказывается недостаточно. Например, когда на устройстве пользователя доступно выполнение команд от имени суперпользователя (root). Этой возможности, как правило, нет в стандартной поставке ОС, но продвинутые пользователи добавляют ее самостоятельно: для использования специфических приложений или для улучшения UX операционной системы. Далее возможен такой сценарий: условно легитимное приложение запрашивает повышенные привилегии для выполнения своей основной функции, а получив эти привилегии, начинает делать то, чего пользователь от него не ожидает. Например, копировать данные из песочниц других приложений. Другой пример — наличие уязвимостей, позволяющих читать содержимое песочницы из другого приложения. В этом случае вредоносному приложению не нужны повышенные привилегии. Оно проэксплуатирует эту уязвимость и получит доступ к незашифрованным данным во внутренней директории целевого приложения. Именно поэтому данные должны быть зашифрованы. Благо сейчас это сделать очень просто и не нужно быть экспертом в криптографии. Достаточно использовать решения вендора и следовать практикам, описанным в официальной документации.

Еще один, не менее интересный тип уязвимостей — отсутствие контроля целостности исполняемых файлов и защиты от модификации. Здесь разработчику тоже достаточно ничего не делать и, соответственно, никакой защиты не будет. Это позволит злоумышленникам модифицировать исходное приложение и распространять его под видом оригинального. Казалось бы, кому может понадобиться скачивать неоригинальное приложение? На самом деле, много кому. Помимо таких банальных потребностей, как вырезание рекламы и механизмов контроля платных функций, пользователям может понадобиться запускать приложения на устройствах с модифицированными прошивками. Такие прошивки очень часто имеют возможность выполнения команд от имени суперпользователя, и банковские приложения, содержащие соответствующие механизмы безопасности, отказываются работать на таких устройствах. Следовательно, нужно убрать из банковского приложения все эти проверки, чтобы оно работало на таких прошивках. Такой деятельностью, как правило, занимаются энтузиасты ради спортивного интереса. Но точно также могут действовать злоумышленники, и тогда в банковском приложении не только исчезнут проверки, но и появится код, который ворует данные для входа в аккаунт. Защититься от таких модификаций довольно сложно, и, как правило, это требует дополнительного приобретения специализированных утилит-упаковщиков, которые усложняют обратную разработку (reverse engineering) и позволяют «потратить» очень много времени злоумышленника на исследование механизмов безопасности. Можно попробовать написать нужные механизмы безопасности самостоятельно, но это требует высокой квалификации, выходящей за рамки компетенции обычного разработчика мобильных приложений.

Отдельно стоит упомянуть уязвимости, связанные с сетевым взаимодействием. Многие разработчики останавливаются на использовании защищенного протокола HTTPS и не навешивают никаких дополнительных защит. При определенных условиях это позволяет злоумышленнику, контролирующему канал связи, выполнить MITM-атаку на приложение и получить конфиденциальную информацию. Один из базовых сценариев такой атаки выглядит следующим образом: при подключении к недоверенной сети Wi-Fi пользователю показывают поддельный captive portal и предлагают установить на устройство SSL-сертификат. После этого злоумышленник может перехватывать весь трафик, который генерирует смартфон пользователя. Для защиты от этой атаки обычно применяют технику закрепления сертификата. Фактически это жестко закодированный в мобильном приложении сертификат или цепочка сертификатов легитимного сервера. Существуют и другие вариации этой защиты, но все они направлены на то, чтобы предотвратить обмен данными с другим сервером.

Также для Android, особенно ранних версий (4.1.1 и ниже), очень характерны уязвимости, связанные с взаимодействием приложений между собой (inter-process communication) и с ненадлежащим использованием возможностей ОС и фреймворка. Долгое время документация по этим возможностям оставляла желать лучшего, а некоторые части и вовсе были не задокументированы. Наряду с отсутствием внятных руководств и описания лучших практик это заставляло разработчиков писать своеобразный код, зачастую переизобретая механизмы, которые уже были в ОС. Очень показательный пример — флаг android:exported, который управляет доступностью какого-либо компонента приложения для вызова другими приложениями. Именно в версии Android 4.1.1 и ниже этот флаг по умолчанию выставлен в true, а значит, все компоненты, где этот флаг не установлен разработчиком, явно будут доступны для вызова другими приложениями. Это может привести к обходу механизмов аутентификации, например экрана с вводом PIN-кода, или к эксплуатации других уязвимостей за счет прямого взаимодействия с теми компонентами, которые разработчик задумал как внутренние и недоступные извне. Такова концепция приложений для Android. В них не должно быть какой-то одной обязательной точки входа и таких точек может быть несколько. Поэтому очень важно уменьшать количество внешних компонентов, а в оставшихся — жестко контролировать любое общение с «внешним миром».

Еще одним, самостоятельным типом уязвимостей можно считать хранение в коде различных ключей доступа к API технических сервисов. Например, систем аналитики и сбора ошибок, возникающих в приложении, облачных баз данных и прочих внешних сервисов. Часто такие сервисы предоставляют ключи с разным типом доступа, так как разработчики этих сервисов понимают, что они будут использоваться в недоверенной среде. Но разработчики приложений по разным причинам все равно оставляют в коде ключи с «лишними» привилегиями. Опасность утечки таких ключей зависит от конкретного случая, но, например, получение серверного ключа для Firebase Cloud Messaging позволит злоумышленнику отправлять произвольные push-сообщения всем зарегистрированным пользователям приложения.

Вымирающие виды уязвимостей

Операционные системы эволюционируют — и уязвимости тоже. Одни исчезают совсем, эксплуатация других осложняется, но все еще остается возможной. Также из-за появления новых механизмов ОС возникают новые уязвимости или перерождаются старые, которые снова заработали благодаря ошибкам в реализации этих механизмов. Одна из таких уязвимостей — CVE-2020-0188. Она позволяла читать файлы из внутренней директории стандартного приложения «Настройки», которое использует механизм Slices, представленный в Android 11. Что же касается исчезающих видов уязвимостей, которые все реже встречаются в приложениях, стоит еще раз упомянуть обход экрана с PIN-кодом путем прямого вызова главного экрана. Почему это стало возможным? Есть несколько факторов:

  1. В какой-то момент Google поменяла значение по умолчанию для флага android:exported, и все компоненты стали недоступны по умолчанию для других приложений, если флаг явно не установлен разработчиком. А позже и вовсе сделала наличие этого флага обязательным.
  2. В официальной документации появились разделы о безопасности приложений, в которых описаны практики правильного использования таких важных механизмов.
  3. Популяризация архитектуры single activity при разработке приложений. На этой архитектуре стоит остановиться немного подробнее, потому что она оказала влияние не только на эту уязвимость. Выше мы говорили, что в приложениях Android обычно нет единой точки входа и они могут быть вызваны несколькими разными способами. Так происходит, потому что у приложения может быть несколько «экранов» (activity, в терминах фреймворка) и если такой экран экспортирован, то его можно запустить независимо от других. Архитектура single activity как раз диктует отказ от нескольких activity в пользу одной, внутри которой живут все остальные экраны ( fragment, в терминах фреймворка). Помимо чисто технического удобства, это позволяет снизить количество точек входа в приложение и организовать контроль входных данных в одной точке, а не на каждом отдельном экране. Другие архитектурные принципы, которые применяются вместе с такой архитектурой, также снижают количество используемых компонентов Android. Таким образом, разработчикам, как правило, не нужно вводить дополнительные services, broadcast receivers и content providers в том количестве, в котором это требовалось раньше. Но для различных специфических задач они все еще нужны, и иногда без них просто не обойтись. В этих случаях помогает документация вендора по лучшим практикам использования тех или иных компонентов с точки зрения безопасности. Да и сама операционная система с каждым годом становится все более нетерпимой к разного рода злоупотреблениям. Из менее банальных примеров вымирающих уязвимостей можно упомянуть небезопасную обработку широковещательных сообщений. Уже года три мы не встречали эту уязвимость в приложениях наших клиентов. И чаще всего это связано с тем, что у приложений нет потребности в обработке каких-то особых видов сообщений. Все, что есть, — это стандартные механизмы, которые, как правило, приходят из стандартных же библиотек и работают в большинстве случаев без ошибок. Такая же участь постигла уязвимость, связанную с подделкой push-уведомлений. На стороне разработчиков остались стандартные механизмы, созданные в соответствии с документацией, а на стороне вендора — ограничение прав на ключи доступа к API для работы с push-сообщениями. А еще разработчики наконец-то осознали, что все, что попадает в приложение, может стать доступно злоумышленникам, и практически перестали оставлять отладочные функции в релизных сборках.

Актуальные виды уязвимостей

Но, несмотря на все усилия Google и сообществ по безопасной разработке, уязвимости в приложениях все еще встречаются. Помимо уже описанных выше уязвимостей, которые условно можно назвать «простыми», потому что они существуют как бы сами по себе, сейчас все чаще встречаются «сложные». Это уже не уязвимости сами по себе, а скорее полноценные атаки, которые объединяют в цепочку несколько уязвимостей и (или) особенностей работы приложения и фреймворка Android. Причин этому несколько. Помимо повышения безопасности самой платформы, растет сложность приложений, и данные, попадающие в них извне, часто проходят довольно длинную цепочку преобразований. А это, в свою очередь, приводит к тому, что на каком-то из этих этапов цепочка эксплуатации может прерваться просто потому, что разработчик для каких-то своих нужд преобразовал данные так, что уязвимость стала неэксплуатируемой. О безопасности он при этом мог вообще не думать. Хороший пример — атака на небезопасную реализацию OAuth в приложении. Разработчики неплохо усвоили, что в недоверенных средах нужно использовать расширение PCKE, однако из-за сложности реализации ошибки все же возникают. В протоколе участвуют три стороны: мобильное приложение, сервер мобильного приложения и сервер провайдера OAuth — целых три точки, где что-то может пойти не так. Например, если сервер провайдера OAuth некорректно проверяет redirect_url (параметр для перенаправления пользователя в мобильное приложение), то злоумышленник может подставить в него свое значение и перехватить код, необходимый для получения токена авторизации на сервере мобильного приложения. Или мобильное приложение может недостаточно хорошо контролировать данные, которые передаются на сервер провайдера OAuth, и тогда злоумышленник может вклиниться в этот процесс и заставить пользователя ввести свои данные на поддельном сайте. Вариантов атаки на эту реализацию много, и некоторые сценарии довольно сложны. Из того, что попадалось мне в программах багбаунти в этом году: атака из десяти шагов, включающих в себя взаимодействие со всеми тремя сторонами, которая в итоге приводит к полному захвату аккаунта пользователя в целевом сервисе, а также получение дополнительных сведений о пользователе от провайдера OAuth путем манипулирования списком данных, запрашиваемых при аутентификации. Повышение сложности приложений также привело к появлению уязвимостей, связанных с экосистемой таких приложений. Зачем тщательно проверять, если ты передаешь данные в приложение, написанное другой командой, и точно знаешь, что там все в порядке? Проблема в том, что приложение может оказаться не тем — по разным причинам. Например, зловредное приложение имеет такой же идентификатор, как и легитимное, скажем com.news.app. Если другое приложение из этой экосистемы не делает никаких дополнительных проверок, а просто полагается на существование в системе такого идентификатора и отправляет ему какие-то чувствительные данные, перед нами экосистемная уязвимость. Работает это и в обратную сторону. Получение данных от «доверенных» приложений без дополнительных проверок может привести к фатальным для пользователя последствиям. Из моих личных примеров: приложение, которое проверяло наличие в системе определенного идентификатора и, если находило его, запрашивало конфигурацию. Это позволяло выставить флаг отладки у первого приложения и заставить его сохранять пользовательские данные в доступном для всех приложений месте. Также актуальными остаются уязвимости, связанные с локальной аутентификацией: PIN-код, биометрия, второй фактор. Их можно обойти из-за ошибок в реализации или из-за недостаточного понимания разработчиками концепций, заложенных в фреймворк. В случае с локальной реализацией входа по PIN-коду разработчики порой забывают сохранять количество использованных попыток входа. В этом случае можно обнулять счетчик попыток простым перезапуском приложения. И это встречается чаще, чем может показаться на первый взгляд. В чуть более сложном варианте помогает перевод системного времени, который также может плохо детектироваться логикой приложений; он приводит к сбросу количества попыток ввода. Обход биометрии чуть более сложен, но все еще возможен, если приложение отображает биометрическое диалоговое окно просто для проверки предъявленных данных. При определенных условиях можно скрыть такое окно и попасть в приложение. Это возможно, потому что предъявление биометрии не связано ни с какими криптографическими операциями с данными приложения и отмена диалога не влияет ни на какие внутренние процессы аутентификации. А возможность обхода второго фактора аутентификации очень сильно зависит от логики приложения. Из недавних примеров — обход второго фактора в TikTok из-за случайного таймаута на сервере при нескольких неправильных попытках входа в определенной последовательности.

Куда все движется

Android не стоит на месте, и его механизмы безопасности постоянно совершенствуются. Но не все проблемы можно решить технически, и порой их приходится решать «административно». Так, начиная с Android 14 приложения, которые таргетируются на версию Android SDK ниже 23 (Android 6.0), не могут быть установлены. Проблема в том, что злоумышленники намеренно занижают версию SDK во вредоносных приложениях, чтобы эксплуатировать хорошо известные недостатки системы благодаря механизму обратной совместимости. Приложения тоже меняются. Появляется все больше кросс-платформенных приложений, упрощается процесс разработки приложения под несколько операционных систем сразу. Но за все нужно платить, и кросс-платформенные приложения, помимо ошибок, характерных для конкретной платформы, добавляют свои особенности поведения, которые также могут быть проэксплуатированы злоумышленниками. Проблема здесь еще в том, что инструменты и библиотеки для разработки таких приложений пока далеки от совершенства, а то и отсутствуют вовсе. Поэтому некоторые функции разработчикам приходится реализовать самостоятельно, что тоже чревато ошибками, особенно при реализации криптографических операций или некоторых протоколов. Разработка таких приложений всегда выполняется на определенном слое абстракции, когда механизмы конкретной платформы скрыты от разработчика. При желании, конечно, он может добраться до этих механизмов и взаимодействовать с ними напрямую. Но тут появляется другая проблема: хороший разработчик приложений для Android вряд ли глубоко разбирается в механизмах безопасности платформы iOS. Верно и обратное. Все эти факты, плюс отсутствие хорошо задокументированных лучших практик безопасной разработки кросс-платформенных приложений приводят к появлению довольно простых и очевидных уязвимостей. Например, в одном из таких кросс-платформенных приложений мне удалось обнаружить несколько ключей доступа к API внешних систем, которых там быть вообще не должно. Они просто не могли бы попасть в приложение в таком виде, если бы оно было разработано с использованием нативного подхода.

Примером незрелости инструментов может служить поддержка формата Hermes для приложений на React Native. Это бинарный формат, в который преобразуется итоговый код JavaScript, содержащий логику приложения. Из-за отсутствия хороших инструментов для декомпиляции этого формата он очень сильно осложнял процесс исследования мобильных приложений. Но поддержка этого формата какое-то время существовала только для приложений Android, и стандартным трюком, который работает и сейчас, было получение итогового кода JavaScript из приложения iOS, если вдруг в версии для Android он был скомпилирован в Hermes.

Короче говоря, война брони и снаряда продолжается. Появляются новые возможности ОС, в них находят уязвимости, эти уязвимости закрываются, но находятся способы обхода защиты. Все это работает как постоянно эволюционирующий живой организм. Я описал лишь малую часть происходящего, чтобы показать, какой путь прошли уязвимости в приложениях для Android и какое влияние они оказали на развитие операционной системы. Разработчикам приложений я бы порекомендовал внимательно следить за новыми механизмами безопасности, которые появляются в Android, и начинать их применять как можно скорее, чтобы защитить пользователей. В свою очередь, пользователям нужно смотреть на происходящее в их устройстве критическим взглядом и помнить, что если вам хоть на секунду показалось, что что-то не так, значит, действительно что-то не так. Все это очень многомерная история и поэтому лучшее, что мы как специалисты по анализу защищенности мобильных приложений можем сделать, — это продолжать искать уязвимости в мобильных приложениях и ОС, совершенствовать способы защиты от них и давать разъяснения разработчикам, чтобы сделать эту часть мира чуточку безопаснее.


Смотрите также