В среднем на Android-устройствах доступно более ста экспортируемых контент-провайдеров. Они поставляются как системными приложениями, так и теми приложениями, которые пользователи устанавливают самостоятельно. Уязвимость в контент-провайдерах автоматически ставит владельца устройства под удар. Более того, кто угодно может обращаться к открытым контент-провайдерам, не имея специально запрашиваемых привилегий. И приложение, которое аккуратно считает твои приватные данные, фактически не проявит никаких признаков вредоносного кода…

 

WARNING

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

 

Сам себе злобный Буратино

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

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

 

Разграничение доступа и контент-провайдеры

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

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

Для более тонкой регулировки можно использовать параметры android:readPermission и android:writePermission. Как ясно из их названий, они позволяют установить отдельно ограничение доступа на чтение или запись. Причем эти параметры имеют больший приоритет, чем более общий параметр android:permission.

Но есть и еще один, более глубокий уровень регулировки доступа. Он позволяет разрешить доступ к определенному набору информации, который поставляет провайдер. На полную катушку в этом случае используется то, что доступ к провайдеру осуществляется через URI bit.ly.

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

 

Проблемы и решения

Итак, мы уже можем сделать предварительные выводы о том, чем нам грозит плохо реализованный контент-провайдер:

  1. Несанкционированный доступ к персональным данным пользователя и чувствительной информации. Причиной может быть открытый контент-провайдер, который установило в систему легитимное приложение с высоким уровнем привилегий. Предположим, мы хотим почитать из своей не совсем легальной программы SMS пользователя. Если мы запросим напрямую разрешение READ_SMS, то привлечем к себе ненужное внимание. Да и человека, который установит программу, читающую SMS, еще нужно поискать. С другой стороны, можно поискать уже зарегистрированный в системе контент-провайдер, который установило привилегированное стороннее или встроенное приложение.
  2. Уязвимости типа SQL injection в провайдерах, работающих с базами данных. К сожалению, фильтрация пользовательского ввода полностью отдана на откуп разработчику мобильных приложений. Более того, «корпорация добра» оставила в документации множество грабель, на которые наступают программисты. Так, в разделе Content Provider Basics Android SDK есть подраздел Protecting Against Malicious Input, на который многие просто не обращают внимания. Разработчики не совсем точно понимают, как работают интерфейсы для обращения к БД SQLite в Android. Например, многие полагают, что повсеместно используемый метод query из класса android.database.sqlite совершенно безопасен. Но это не соответствует действительности, исследователи из MWR Labs достаточно подробно описали проблему и даже нашли несколько уязвимостей в устройствах Samsung. Они также выпустили удобный фреймворк Mercury, который позволяет находить такие дырки в приложениях. Многие разработчики не используют prepared statements, хотя они были в Android с первой версии API.
Интерфейс командной строки MWR Mercury
Интерфейс командной строки MWR Mercury
 

Пишем свою утилиту для анализа контент-провайдеров

Ранее я уже упомянул Mercury от MWR Labs. Это замечательный набор инструментов, и я рекомендую им пользоваться. К сожалению, лично меня повергает в уныние один взгляд на пользовательское лицензионное соглашение, под которое попадает этот продукт. Кроме того, лучше всего усвоить материал на практике, тем более когда программирование не представляет существенной сложности.

Напишем свое приложение под Android для анализа контент-провайдеров. Что мы сделаем?

  1. Получим информацию обо всех зарегистрированных в операционной системе контент-провайдерах.
  2. Определим, какие из этих провайдеров не защищены с помощью привилегий.
  3. Немного поэкспериментируем ;).

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

getPackageManager().getInstalledPackages(PackageManager.GET_PROVIDERS).size();
  ...
    for (PackageInfo pack : getPackageManager().getInstalledPackages(PackageManager.GET_PROVIDERS)){
providers = pack.providers;
if (providers != null){
for (ProviderInfo provider : providers){
if (provider.authority != null){
//provider.authority
//provider.readPermission
//provider.writePermission
...
}
}
}
}

Объект типа ProviderInfo содержит всю необходимую нам информацию. Мы получим соответствующий ContentResolver и обработаем результат и возможные исключения.

...
try
{
Uri uri = Uri.parse(authority); // Получаем URI вида content: //
...// Подготавливаем параметры для обращения к контент-провайдеру
// Запрашиваем данные у контент-провайдера. В ответ получим курсор или исключение
Cursor c = getContentResolver().query(uri, prj, selection, sel_args, null);
int col_c = c.getColumnCount();
...
// Проходим курсором по результатам запроса
if (c.moveToFirst()) {
do {
// Пробежимся по всем колонкам
for (int i=0; i<col_c; i++)
{
// Если в колонке, возможно, картинка
if (Columns[i].toLowerCase().contains("image"))
{
...
byte[] blob = c.getBlob(i);
// то показываем в hex-виде (можно и отобразить на экране устройства)
s +=  bytesToHexString(blob);
}
else
{
try
{
// Если содержимое колонки не отображается как текст
s += c.getString(i);
}
catch (Exception e)
{
// показываем как hex
byte[] blob = c.getBlob(i);
s += bytesToHexString(blob);
}
}
}
} while (c.moveToNext());
}
}
catch(Exception e)
{
// Чаще всего исключения возникают из-за недостатка прав или ошибки в запросе, но в любом случае они очень информативны
s += e.getMessage();
...

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

Главное окно приложения. Позволяет выбрать параметры для вызова контент-провайдера
Главное окно приложения. Позволяет выбрать параметры для вызова контент-провайдера
Выпадающий список. Содержит перечень всех доступных контент-провайдеров с указанием установленных разрешений на чтение и запись
Выпадающий список. Содержит перечень всех доступных контент-провайдеров с указанием установленных разрешений на чтение и запись
Результат запроса к провайдеру settings
Результат запроса к провайдеру settings

Раз утилита готова — как я и обещал, немного поэкспериментируем. Я использовал обычный смартфон, старый Samsung Galaxy S с последней официальной прошивкой и некоторым набором самых распространенных приложений.

В списке контент-провайдеров своего телефона ты самостоятельно можешь найти что-то забавное. Например, мое внимание привлек провайдер com.sec.provider.facekey. В advisory MWR Labs про него ничего не сказано, тем не менее он представляет определенный интерес. Дело в том, что он устанавливается и используется системой «биометрической» блокировки экрана по снимку лица. Удивительно, что привилегии, запрещающие чтение и запись, в данном случае не установлены. Попробуем передать провайдеру SQL injection вектор «* from sqlite_master--».

Биометрическая блокировка экрана (Samsung Galaxy S)
Биометрическая блокировка экрана (Samsung Galaxy S)

 

С интересом узнаем, что мы получили доступ к базе данных с табличкой facefeature следующего вида:

CREATE TABLE facefeature (_id integer primary key autoincrement, facetime long, facefeature blob, faceimage blob);

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

Немного порывшись в списке, можно считать настройки телефона из провайдера com.settings (например, content://com.settings/secure). Это несмотря на то, что разрешения READ_SETTINGS мы не имеем.

Интересный результат дает обращение к «content://com.google.settings/sqlite_master--» (что ж, и Google промахивается):

Authority: content://com.google.settings/sqlite_master--
Projection:null
Selection:null
Selection args:null
type:name:tbl_name:rootpage:sql:
table;android_metadata;android_metadata;
3;CREATE TABLE android_metadata (locale TEXT);
table;partner;partner;
4;CREATE TABLE partner (_id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT UNIQUE ON CONFLICT REPLACE,value TEXT);
index;sqlite_autoindex_partner_1;partner;
5;null;
table;sqlite_sequence;sqlite_sequence;
6;CREATE TABLE sqlite_sequence(name,seq);
index;partnerIndex1;partner;
7;CREATE INDEX partnerIndex1 ON partner (name);

Дальнейшие эксперименты я оставляю читателю. Все исходные тексты утилиты доступны на GitHub.

 

Как задаются свойства контент-провайдера

Если обратиться к Android SDK (где весь процесс расписан очень подробно и по шагам), то видно, что для того, чтобы зарегистрировать свой контент-провайдер, тебе придется добавить описание провайдера в файл AndroidManifest.xml в секцию. Параметров при этом можно указать множество bit.ly, но мы рассмотрим лишь важные для нас:

<provider
   android:authorities="com.test.provider"
   android:name=".provider.MyContentProvider"
   android:exported="true|false">
</provider>

Важная особенность — доступ к данным будет осуществляться при помощи специального URI со схемой «content». Параметр android:authorities является уникальным идентификатором провайдера и представляет собой первую часть URI, вторая часть будет указывать на то, какие именно данные мы хотим получить. Параметр android:exported показывает, доступен ли провайдер для других приложений. Уже тут есть маленькая особенность, которая может испортить жизнь разработчику. Этот параметр в версиях Android до 16-й включительно (Android 4.1 JELLY_BEAN) по умолчанию установлен в «true», и все контент-провайдеры экспортируются, естественно, такая ситуация не безопасна. Но только с версии 17 (Android 4.2 JELLY_BEAN_MR1) в ОС было внесено исправление, и теперь, чтобы экспортировать провайдер, необходимо самостоятельно изменить используемый по умолчанию «false».

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

content://com.test.provider/give_me_data_with_id/123

Фантазия разработчика при написании провайдера мало чем ограничена, дело в том, что обязательно нужно переопределить всего лишь шесть абстрактных методов (query(), insert(), update(), delete(), getType(), onCreate()). При этом никто не запрещает сделать эти методы пустыми, возвращать на любой запрос константу, результат чтения файла или обращения к сети. Тем более если твой провайдер предоставляет доступ к данным только на чтение, то методы insert(), update() и так далее ему совсем не нужны.

Логотип Mercury
Логотип Mercury

И хотя никто не принуждает программиста прятать «под капотом» своего контент-провайдера базу данных, но очень часто тут встречается хорошо знакомая всем разработчикам мобильных приложений для Android SQLite. Тем более что используемый для получения данных ContentResolver всегда возвращает объект типа Cursor, что тонко намекает…

 

MEMENTO MORI

Итак, мы рассмотрели одно из слабых мест в Android-приложениях. Я постарался показать, насколько важно использовать по максимуму возможности по разграничению доступа, которые предлагает операционная система.

Разработчикам мобильных приложений хочется напомнить, что, написав контент-провайдер, вы принимаете решение поделиться информацией, поэтому стоит подумать, кто и в каком объеме сможет получить к ней доступ. Также многие забывают, что уязвимостям типа SQL injection подвержены не только веб-приложения. Поэтому санитизацию пользовательского ввода и использование prepared statements никто не отменял, даже под Android.

 

 

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии