Последние 6 лет я работаю экспертом по информационной безопасности в Одноклассниках и отвечаю за безопасность приложений.


Мой доклад сегодня — о механизмах межпроцессного взаимодействия в Android и уязвимостях, связанных с их неверным использованием.



Но сначала пара слов о том, как появился этот доклад.


Наверное, вы уже заметили, что на каждом Heisenbug есть как минимум один доклад про безопасность, например про XSS или поиск уязвимостей в веб-приложениях. Тема мобильной безопасности осталась не охвачена, хотя мы живем в 2020 году, и аудитория мобильных приложений уже давно превысила аудиторию веба. Мне хотелось выбрать тему из мира мобильной безопасности — достаточно конкретную, чтобы выдержать формат технического доклада, но при этом достаточно распространенную.


Проблемы безопасности на мобилках


Единым местом концентрации всех знаний про безопасность приложений, и мобильных в частности, является проект OWASP (расшифровывается как Open Web Application Security Project), который раз в несколько лет публикует топ-10 самых распространенных рисков для мобильных приложений.



На картинке сверху последняя версия OWASP Mobile Top 10. Первое место в нем занимает категория «неправильное использование платформы». Это довольно общее определение, обратимся к пентест-отчетам за конкретными примерами.


Positive Technologies в прошлом году на основе статистики проводимых аудитов опубликовала отчет о состоянии дел в области безопасности мобильных приложений.


Согласно этому отчёту,


  • 38% приложений на Android,
  • 22% приложений на iOS

содержат те или иные уязвимости, вызванные неверной реализацией межпроцессного взаимодействия.


38% — это каждое третье приложение!


Для мотивации заглянем в публичные данные из bug bounty программ: сколько было опубликовано похожих уязвимостей и сколько за них заплатили. Точной классификации категорий багов нет, но поискав по ключевым словам на HackerOne, видим что таких багов на Android находится много и оценивают их довольно высоко.



Надеюсь, вы уже все хотите узнать про то, что такое уязвимости межпроцессного взаимодействия.


Начнем с определения!


Межпроцессное взаимодействие


Приложение на Android имеет компонентную модель и может состоять из компонентов нескольких типов.


  • Activity — компонент, отвечающий за UI приложения; один экран, который видит пользователь.
  • Service отвечает за выполнение операций в бэкграунде. Он может продолжать работу даже, когда приложение не отображается.
  • BroadcastReceiver обеспечивает обмен сообщениями между приложениями и операционной системой.
  • ContentProvider — компонент для доступа к данным, которые хранит приложение.

В одном приложении может быть больше, чем один компонент каждого типа. В контексте этого поста важно, что некоторые компоненты могут быть доступны для других приложений на этом же устройстве.


То, какие компоненты определены в приложении, доступны они или нет, какими свойствами обладают, описано в файле AndroidManifest. Это XML-файл, который поставляется вместе с приложением.



Таким образом приложения на устройстве предоставляют интерфейс, который позволяет им общаться друг с другом путем вызова компонентов, объявленных как публичные (их чаще называют экспортируемые). Это фундаментальная возможность платформы Android. Она позволяет организовать ту самую экосистему из приложений на телефоне, к которой мы так привыкли. К примеру, этой возможностью пользуется камера, когда вы отправляете фото сразу из камеры в мессенджер или в почту. На этой же функциональности построена возможность для приложения дожидаться смс и автоматически подставлять код подтверждения за вас, или возможность для одного приложения прочитать данные другого, например список телефонов с телефонной книжки.


Разработчик не может делать никаких утверждений об остальных приложениях на этом же устройстве и должен предполагать, что рядом может оказаться зловредное приложение, которое будет нарушать существующие контракты и пытаться злоупотребить возможностями платформы. Другими словами, мы всегда должны подозревать, что у пользователя установлен какой-то вирус.


Частое возражение, которое я слышу от разработчиков, что такая угроза существует теоретически, но на практике она маловероятна, поскольку приложения всегда устанавливаются из Google Play, а там они предварительно проверяются. Сложно себе представить, как вообще вирусы могут оказаться на устройстве пользователя.


Это не так.


Google, конечно, совершает феноменальные усилия, чтобы модерировать приложения и не допускать подозрительные или опасные в Стор. Но тем не менее, мы постоянно видим новости о том, что обнаружена очередная сеть приложений, которая признана вредоносной и удалена. До момента удаления такие приложения набирают миллионы установок. Быстрый поиск выдаёт нам десятки таких новостей за 2019 год, то есть проблема реальна, и вероятность того, что какой-то пользователь себе вирус установит, существует. Можно даже с уверенностью сказать, что у части из пользователей вашего приложения вирус установлен.


Механизмы безопасности IPC


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


Вспомним задачу: у нас есть приложение, которое выставляет наружу какую-то часть своей функциональности. Мы предполагаем, что рядом с этим приложением установлено вредоносное приложение, которое пытается злоупотребить нашей функциональностью.


Android предоставляет приложению возможность ограничить доступ к своим компонентам.


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


Если exported=true, то ваш компонент доступен для остальных приложений. Если exported=false, то компонент недоступен.


<activity 
        android:name = ".ViewStatement" 
        android:exported = "true" 
        android:label = "@string/title_activity_view_statement" > 
</activity>

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


Другим уровнем системы разграничения доступа для межпроцессных вызовов на Android является модель пермиссий. Приложение может определять какие другие приложения получат доступ к его функциям или данным несколькими способами:


  • Signature-based основан на подписи приложения, то есть все приложения, которые подписаны одним и тем же сертификатом, могут вызывать компоненты друг друга.
  • sharedUserId: два приложения могут в явном виде в манифесте указать, что они пользуются одним и тем же user ID, то они могут обращаться к данным друг друга
  • Custom permissions позволяют определять собственные permission для отдельных компонентов.

Как я уже сказала, проблема для разработчиков заключается в том, что все эти слои разграничения доступа накладываются друг на друга, и при беглом взгляде на исходный код не всегда понятно, доступен ли наш компонент, и если да, то для каких приложений.


В чем же проблема? Чтобы разобраться, посмотрим, как вообще устроены эти самые вызовы между приложениями в Android.


Единицей межпроцессного взаимодействия является нечто под названием Intent.


Это некая абстракция, которая определяет намерение одного компонента вызвать другой. Intent характеризуется типом действия, который будет совершён и типом данных, с которыми эти действия будут совершаться. В Intent могут передаваться дополнительные данные и возвращаться результат исполнения.


Что вызывает путаницу? Intent-ы бывают явные (explicit) и неявные (implicit).


  • Явные — однозначно задан компонент, который будет его обрабатывать.

new Intent(getApplicationContext(), SecondActivity.class);

  • Неявные определяют только тип действий, который мы хотим совершить. Например, мы хотим отредактировать картинку и предоставляем Android возможность самому выбрать из имеющихся редакторов. То, что получатель интента не определен, вносит некоторые риски и накладывает ограничения при разработке приложений. О них чуть позже.

Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(“https://ok.ru/“));

Intent-filter декларируется в манифесте и определяет, какие именно неявные интенты компонент может обрабатывать.


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



Требования безопасности для приложения


Мы выяснили, как устроены вызовы между приложениями на андроиде, и какие средства предоставляет платформа для контроля доступа к компонентам приложения. Самое время сформулировать требования безопасности для приложений. Первое, что мы узнали, — компоненты могут быть доступны извне и также они могут возвращать какие-то данные вызывающим компонентам. Из этого следует, что компоненты, возвращающие конфиденциальные данные, не должны быть экспортированы, то есть не должно быть возможности вызвать их из другого приложения.


Также мы узнали, что мы не можем гарантировать получателя неявных интентов или (если мы их обрабатываем) предсказать отправителя. Это значит, что мы не можем доверять данным, которые переданы в интент по умолчанию. Мы должны трактовать их как параметры, контролируемые атакующим, то есть возможно измененные, и предварительно проверять их.


И поскольку мы не знаем, кто получит наш неявный интент, мы не можем передавать в нем секретные данные.


Теперь обратимся к практике.


Примеры уязвимостей и их последствия


Тренироваться мы будем на специальном обучающем приложении — InsecureBankV2 — с небольшими дополнениями. Исходный код приложения и инструкции по поиску уязвимостей в нем доступны на гитхабе. Всем тем, кому будет интересно продолжить разбираться в этой теме по окончанию доклада, я советую его скачать и почитать.


Пример 1


Начнём с самого простого, а именно с того, как приложение хранит данные. Мы уже помним, что для того, чтобы обращаться к данным, приложения используют ContentProvider. Посмотрим в AndroidManifest InsecureBankV2 и попытаемся найти там определения провайдеров.


<provider 
     android:name =".TrackUserContentProvider" 
… 
     android:exported = "true" > 
</provider>

Там объявлен провайдер с явным указанием exported=true, то есть другое приложение может обратиться к этому провайдеру и прочитать или записать данные.


Давайте сначала проверим, действительно ли это так. Я в целях тестирования буду где возможно использовать adb — так нагляднее. Надо понимать, что те же действия может совершить любое приложение на том же устройстве.


Прочитаем содержимое ContentProvider.


$adb shell 
 content query --uri 
 content://<…>/trackerusers


Всегда ли такое поведение является уязвимостью? Конечно нет, зависит от того, какие данные предоставляет провайдер.


Как сделать правильно? Правильнее было бы определить кастомные пермиссии для доступа на чтение и запись в этот провайдер, что позволило бы контролировать какие приложения получают доступ к данным. Например:


<provider 
    android:name =".TrackUserContentProvider" 
… 
    android:exported = "true" 
    android:readPermission = “my.custom.read.permission” 
    android:writePermission = “my.custom.write.permission" 
> 
</provider>

Пример 2


Давайте снова заглянем в манифест и поищем там компонент, который может быть вызван из другого приложения.


<activity 
        android:name = ".PostLogin" 
        android:label = "@string/title_activity_post_login" > 
        <intent-filter> 
                <action android:name = "customAction" /> 
        </intent-filter> 
</activity>

Помните, я говорила, что добавление IntentFilter-а автоматически делает активность доступной извне? Это мы видим на примере активности PostLogin, для которой объявлен фильтр.


Проверим, что эта активность действительно может быть вызвана, причём не только в ответ на интент, который матчится с этим фильтром. Снова сделаем вызов через adb — попросим application manager запустить активность по полному имени. Выполним такую команду.


$adb shell 
       am start -n <…>/.PostLogin

Действительно видим, у нас на эмуляторе запустилась эта активность.



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


Последствия


Если какой-то из компонентов приложения не предназначался быть доступным, но может быть вызван извне, то существует возможность обойти логику работы приложения. Типичный пример — экраны с дополнительным подтверждением. Например, мессенджеры или интернет-банки требуют дополнительный пин-код для того, чтобы войти в приложение, даже если телефон разлочен. И если активность, которая показывается после этого дополнительного подтверждения, экспортируемая, то мы можем вызвать ее напрямую и таким образом обойти запрос пин-кода.


Другим примером возможных последствий является злоупотребление привилегиями, которые выданы нашему приложению. Приложение запрашивает привилегированный доступ к данным пользователя или возможностям устройства для того, чтобы совершать полезную для пользователя работу. Зловредное приложение с меньшей вероятностью получит разрешение от пользователя на такой же доступ. Но если приложение, которое совершает какие-то потенциально опасные вызовы, экспортирует компоненты, которые эти вызовы совершают, то зловредное приложение может воспользоваться ими, чтобы повысить свои привилегии.


Частный случай такого привилегированного доступа — доступ в приватную папку приложения. Как вы знаете, приложения на Android выполняются в песочнице, и каждому приложению выделена область файловой системы, которая другим приложениям недоступна. Если путь, по которому приложение читает или пишет в файл, может быть изменен, то другое приложение на этом же устройстве получает возможность возможность доступа к приватной папке уязвимого приложения.


Типичным местом концентрации таких уязвимостей являются активности, которые реализуют возможность «поделиться» в приложении — как правило, они получают путь к тому, что надо отправить, в интенте. Занятные последствия таких ошибок связаны еще с особенностями Android NDK. Android позволяет использовать код на C/C++ при разработке приложения. Скомпилированные нативные библиотеки (.so) упаковываются в apk и после установки приложения размещаются в приватной папке рядом с данными. Это значит, если мы научились записывать произвольные файлы в приватную папку, то мы можем и переписать .so-файлы библиотек и выполнять произвольный код в контексте приложения. Фактически, это уязвимость типа RCE — Remote Code Execution, и предоставляет атакующему полный контроль над уязвимым приложением.


И еще пример. Представим себе возвращающую результат экспортируемую активность. Если доступ к ней не ограничен, то вызвать ее и следовательно получить результат может произвольное приложение из тех, что установлены на том же устройстве. Если в качестве результатов мы возвращаем данные, которые не должны быть разглашены, то наше приложение уязвимо.



Давайте рассмотрим снова пример. Вернёмся к нашей активности post_login, которая, как мы уже выяснили, может быть вызвана извне. Посмотрим в код.


Intent result = new Intent(); 
result.putExtra("session", sessionToken); 
result.putExtra("uname", username); 
setResult(RESULT_OK, result);

Активность использует сохраненные в приватной папке данные для того, чтобы получить новую сессию и вернуть ее вызвавшему компоненту. В случае, если активность была вызвана сторонним приложением, оно получит доступ к аккаунту пользователя.


Пример 3


И третья разновидность уязвимостей, которые можно встретить в Android-приложениях, связана с неверным использованием широковещательных рассылок. Помните, мы говорили про неявные интенты — для них не указывается явно компонент, который будет вызван. В случае с широковещательными рассылками это тоже работает, то есть можно не указывать в явном виде получателя, а предоставить андроиду разослать эти оповещения, основываясь на зарегистрированных получателях.


Посмотрим снова на InsecureBankV2 и найдём там такой кусочек кода.


private void broadcastChangePasswordSMS (String phoneNumber, String pass) {
...
Intent smsIntent = new Intent(); 
smsIntent.setAction("theBroadcast"); 
smsIntent.putExtra("phonenumber", phoneNumber); 
smsIntent.putExtra("newpass", pass); 
sendBroadcast(smsIntent); 

…
}

Что здесь происходит? Мы видим, что создается интент, при этом не указан компонент, который этот интент будет обрабатывать, то есть он является неявным. Мы добавляем номер телефона и пароль пользователя в дополнительные параметры интента. Опасность здесь в том, что другое приложение на устройстве пользователя может зарегистрировать соответствующий BroadcastReceiver и тоже получить эти данные. Для этого нужно зарегистрировать BroadcastReceiver и соответствующий фильтр.



Посмотрим, что это работает. Запустим в левой части экрана InsecureBankV2, а в правой части экрана наше зловредное приложение, которое подслушивает данные пользователя. Как только пользователь нажмет на Change Password, отправится этот неявный интент. И поскольку зловредное приложение зарегистрировало свой BroadcastReceiver, оно перехватит данные пользователя.



Автоматизация


Итого, мы рассмотрели три класса проблем, связанных с неправильной реализацией межпроцессных взаимодействий на Android, и поняли, что тестировать это все довольно трудоемко: требуется изучать манифест приложения и каждый объявленный компонент, иногда даже заглядывать в исходники. Поэтому попытаемся разобраться, есть ли возможность автоматизировать эту задачу.


Когда говорят про автоматизацию тестирования безопасности, то обычно делят инструменты на две группы: динамические и статические сканеры.


Динамические сканеры


Динамические сканеры тестируют работающее приложение, отправляя ему тестовые запросы и ожидая определённой реакции.


Статические сканеры применяют статический анализ исходного кода для поиска заранее известных паттернов в коде, которые могут означать наличие уязвимости.


Среди всех динамических сканеров, которые вообще существуют для тестирования безопасности на Android, самым распространенным является Drozer. Drozer — проект с открытым исходным кодом, поддерживаемый F-Secure.


Сам сканер представляет собой клиент-серверное приложение. В качестве сервера выступает то, что в Drozer называется консоль — скрипт на питоне, запущенный у вас на компьютере. В качестве клиента — Drozer Agent, который устанавливается на устройство или эмулятор. Соответственно, консоль дает агенту команды, а агент эти команды передаёт тестируемому приложению и полученный ответ передаёт в консоль.


Чем нам может помочь Drozer? Drozer не позволяет написать полностью автоматические тесты для поиска обсуждаемого типа проблем, но поможет автоматизировать часть работы, которую мы выполняли, когда изучали манифест и искали объявление общедоступных компонентов в исходниках приложения.



На слайде получение информации обо всех доступных извне компонентах или о том, какие права и какой доступ предоставляется для каждой активности.



Статические сканеры


В другом углу ринга — статические анализаторы. И в этом смысле Android-разработчикам повезло. Во-первых, есть официальный Android Lint. Про Android Lint подробно написано в документации для разработчиков.


А я хотела бы подробнее рассказать про Find Security Bugs. Это проект с открытым исходным кодом, представляющий собой набор ориентированных на поиск уязвимостей правил для популярного статического анализатора SpotBugs (aka FindBugs). Find Security Bugs содержит в том числе специфичные для андроида проверки. Для того, чтобы интегрировать его в свой проект, нужно буквально добавить три строки в gradle скрипт, примерно как на слайде.



Итак, запустим Find Security Bugs на том же самом InsecureBankV2 и посмотрим, что он нашёл.



Мы видим сообщение о том, что broadcast может быть получен зловредным приложением. Смотрим код: строчка, на которую он ругается, это та же самая проблема, которую мы уже нашли в предыдущих примерах. То есть это рассылка данных пользователя с использованием неявного интента. Find Security Bugs объясняет, что пошло не так и как проблему нужно исправить.



На скриншоте GUI, который поставляется вместе с FindBugs, но, как я сказала, те же самые подсвеченные строчки с багами и рекомендациями по исправлению можно увидеть из IDE, например в Android Studio.


FindBugs позволяет также расширять набор правил, которые используются для обнаружения уязвимостей. Я рассказывала об этом на прошлом Heisenbug, доклад можно посмотреть на YouTube.


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


Подведем итоги


В докладе мы рассмотрели распространенный тип уязвимостей в приложениях под андроид, связанный с неверным использованием механизмов межпроцессного взаимодействия.


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


  1. Не экспортировать компоненты, возвращающие конфиденциальные данные.
  2. Не доверять данным, переданным с интентом в экспортируемые компоненты.
  3. Не передавать секретные данные в неявных интентах.

Нарушение этих требований может привести к серьезным последствиям для безопасности приложения. К сожалению, особенности платформы провоцируют появление подобных уязвимостей, поэтому важно учесть их в процессе тестирования.