Содержание статьи
Первоначальное исследование
Первое упоминание об этой уязвимости было опубликовано еще в 2015 году на конференции USENIX Security Symposium 2015, но, к сожалению, не получило широкой огласки и не привлекло к себе должного внимания. Когда мы ознакомились с материалами исследования, первой мыслью было «Да ладно, не может это так работать, это уж слишком. Два года прошло, уже закрыли давно, наверное». Реальность оказалась страшна.
Мало того, что все описанное в статье работает до сих пор, — появилась еще одна, обнаруженная нами, уязвимость, которая делает эксплуатацию намного легче и проще. В статье мы приведем адаптированную информацию из доклада, расскажем про обнаруженную нами уязвимость и попробуем разобраться, как можно защититься от подобных атак.
Немного теории
Если ты профессиональный Android-разработчик, вносишь правки в исходный код Android или пачками репортишь баги, можешь смело пропустить этот раздел и переходить к основной части. В противном случае немного освежим память и рассмотрим, как устроено управление задачами и основные элементы, которые нам потребуются в дальнейшем в системе Android.
Основной компонент, с которым взаимодействует пользователь при работе с приложением, — это Activity. Каждая Activity — это отдельный графический экран со своими элементами, именно их и видит пользователь. Каждое приложение имеет несколько Activity для различных действий, то есть каждый новый экран — это отдельная Activity. Все они описаны в файле манифеста приложения.
Все Activity, которые прошел пользователь за время работы с приложением, располагаются внутри сущности, называемой Task. Activity в Task хранятся в виде строгой последовательности (стека), называемой back stack. При открытии каждого нового экрана создается новый экземпляр Activity и располагается системой на верхушке этого стека. Таким образом, при нажатии на кнопку «Назад» Activity, которая была наверху стека, закрывается (уничтожается) и отображается Activity, которая была под ней. Отсюда и название — back stack.
Activity, которую пользователь видит на экране устройства, находится на переднем плане и называется Foreground Activity, а таск, внутри которого находится эта Activity, — Foreground Task. В один момент времени в системе может быть только один Foreground Task и Foreground Activity, все остальные выполняются на заднем плане, то есть в Background. При переходе на задний план Task становится неактивным, состояние его back stack сохраняется в неизменном виде. Таким образом, когда пользователь снова вернется в приложение, состояние останется тем же, и он сможет возвращаться к экранам в том порядке, в каком их открывал.
Activity внутри back stack могут относиться не только к запущенному приложению, но и к сторонним приложениям. Всем известна ситуация, когда из одного приложения мы можем открыть, к примеру, галерею и выбрать новую фотографию на аватарку. Для разработчика приложения нет необходимости самому реализовывать функцию выбора и предварительного просмотра изображения, для этого достаточно вызвать Activity из нужного приложения (или предоставить выбор приложения пользователю).
Таким образом, при вызове функции сторонней программы в back stack помещается ее Activity, и при нажатии на кнопку Back пользователь вернется к приложению, с которым работал ранее. Это сделано для бесшовной интеграции, чтобы создавалось ощущение работы с единым приложением и не было необходимости переключаться между программами для выполнения одноразовых операций.
Важный атрибут taskAffinity
характеризует, к какому таску должна присоединиться Activity при запуске. Он представляет собой строку, которая либо определяется в манифесте приложения свойством android:taskAffinity
, либо по умолчанию равна ID приложения в системе (applicationId
). Affinity таска определяется значением taskAffinity его root Activity (нижней в стеке). Если явно указывать значение taskAffinity, то можно заставить запускаться Activity в рамках произвольного таска. Таким образом, каждое приложение может породить произвольное количество тасков.
WARNING
Техника подмены приложений работает во всех версиях системы, исправлений нет до сих пор. Материал адpесован специалистам по безопасности и тем, кто собираeтся ими стать. Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
Возвращаемся во вредоносное приложение
За загрузку Activity в Android отвечает Activity Manager Service (AMS). Существует несколько способов запустить Activity в новом таске:
- определить
launchMode="singleTask"
в манифесте приложения у необходимой Activity; - при запуске Activity указать FLAG_ACTIVITY_NEW_TASK.
Вот что происходит в системе, когда ты нажимаешь на значок приложения.
- Если экземпляр Activity уже существует, то AMS находит его и выводит на передний план, а не запускает новый.
- Если требуется создание Activity, то AMS выбирает Task, в который необходимо «положить» созданную Activity. Для этого AMS пытается найти «совпадающий» Task. Activity «совпадает» c Task, если у них указано одинаковое свойство taskAffinity. Если найдено совпадение, сервис кладет новую Activity на верхушку стека в выбранный Task.
- Если же совпадений не найдено, сервис создает Task, и новая Activity становится root Activity.
Теперь, обладая тайными знаниями, попробуем заставить систему переместить запускаемую Activity на стек условно вредоносного приложения. Предположим, что мы хотим атаковать приложение TargetApp и в числе его возможностей есть просмотр видео при помощи приложения PlayerApp (либо пользователь выбирает это приложение для просмотра из выпадающего списка). При этом в интенте, который запускает Activity стороннего приложения, присутствует FLAG_ACTIVITY\_NEW\_TASK
или в сторонней Activity объявлен launchMode="singleTask"
, то есть заданная функция запускается в отдельном таске.
Intent intent = new Intent(Intent.ACTION_VIEW, introUri);
intent.setDataAndType(introUri, "video/mp4");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Также на устройстве находится вредоносное приложение (Malware), которое, помимо того что выполняет вполне легитимные функции, при запуске или при включении устройства создает в системе Background Task с taskAffinity="PlayerApp"
.
Вот как выглядит наш AndroidManifest.xml
:
<activity
android:name=".MainActivity"
android:taskAffinity="com.player.app"
android:exported="true"
android:excludeFromRecents="true"
</activity>
А вот MainActivity.java
:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState == null) {
moveTaskToBack(true);
} else {
initUi();
}
}
Запускаем TargetApp и выбираем просмотр видео. Попробуем пошагово проследить, что произойдет.
- Activity Manager Service понимает, что ему нужно запустить Activity приложения PlayerApp.
- Запущенного экземпляра такой Activity нет в системе, переходим к следующему шагу.
- Пытаемся найти в системе Task с соответствующим taskAffinity. AMS находит наш запущенный вредоносный Task и, следуя своей логике, запускает Activity плеера, помещает ее на верхушку стека и выводит на передний план.
Таким образом, запущенная Activity приложения PlayerApp оказывается на верхушке стека условно вредоносного Task. После просмотра видеоролика пользователь нажимает Back, система уничтожает верхнюю Activity и выводит на передний план лежащую под ней (Mal-Activity 2). Пользователь оказывается во вредоносном приложении, которое, в свою очередь, может имитировать интерфейс приложения TargetApp.
Подменяем приложение при запуске
Описанный выше способ имеет свои недостатки: необходимо, чтобы функциональность, реализуемая сторонними приложениями, обязательно запускалась в новом Task, либо в приложении, которое мы указываем как taskAffinity для вредоносной задачи, либо при запуске из приложения, которое мы атакуем. А вот бы сделать так, чтобы сразу запускать вредоносное приложение и вообще не зависеть от реализации атакуемого приложения. Не вопрос! Официальный Android API позволит нам сделать и это.
По умолчанию, как только Activity запускается и ассоциируется с таском, эта связь сохраняется на всем протяжении жизненного цикла Activity. Однако Android API позволяет указать вместе с taskAffinity свойство allowTaskReparenting
таким образом, что при появлении в системе Task с аналогичным указанному taskAffinity эта Activity сразу перемещается на верхушку его back stack. Пока в системе не будет зарегистрирован такой Task, Activity будет запускаться в рамках своего приложения. Выглядит очень интересно, попробуем проэксплуатировать разобраться.
Итак, в системе, в Background, запущен вредоносный Task, в его стеке находятся root Activity (Mal-Activity 1) и Mal-Activity 2, имитирующая интерфейс приложения, которое хотим подменить. Также при описании Mal-Activity 2 указаны параметры taskAffinity подменяемого приложения и allowTaskReparenting
.
<activity
android:name=".Mal-Activity 2"
android:taskAffinity="com.target.app"
android:exported="true"
android:allowTaskReparenting="true"
android:excludeFromRecents="true"
</activity>
Все приложения, которые запускаются из лаунчера, имеют флаг FLAG_ACTIVITY\_NEW\_TASK
, то есть в новом таске. Пользователь хочет открыть свое любимое приложение, нажимает на иконку. Так как запущенного таска с таким taskAffinity в системе нет, он создает новый Task и запускает в нем Activity. В это же время система регистрирует появление Task с taskAffinity, который указан в Mal-Activity 2, и, в соответствии с ожидаемым поведением, помещает вредоносную Activity на верхушку стека и переводит ее в Foreground. Дело сделано, вместо исходной Activity пользователь видит вредоносную. Для него это абсолютно прозрачно: нажимаем иконку — загружается приложение со знакомым интерфейсом. Activity из оригинального приложения пользователь не видит вообще.
Подменяем приложение при запуске. Версия 2
В описанном случае есть интересный нюанс: если пользователь, находясь на подмененном экране, нажмет Back, система закроет вредоносную Activity и пользователь увидит экран обычного приложения. Можно ли как-то этого избежать и контролировать не только запускаемую Activity, но и Task, в котором она запущена? Больше власти никогда не повредит, давай разбираться.
Что должно произойти при нажатии на иконку приложения в Launcher, если Task с таким taskAffinity уже существует в системе? «Да все просто, мы же говорили об этом выше, AMS должен будет запустить экземпляр Activity и поместить на верхушку стека выбранного Task», — скажешь ты. А вот и нет, из-за бага в Android, который немного нарушает логику работу AMS, поведение будет несколько иным.
Как и в предыдущих случаях, на устройстве в Background запущен таск с taskAffinity приложения, которое мы хотим подменить. При этом больше ничего дополнительно указывать не нужно, AMS все сделает за нас.
<activity
android:name=".Mal-Activity 1"
android:taskAffinity="com.target.app"
android:exported="true"
android:excludeFromRecents="true"
</activity>
В этом случае при нажатии на иконку подменяемого приложения AMS просто выведет на Foreground Task с указанным taskAffinity. То есть Activity подменяемого приложения вообще не будет запущена. Для пользователя опять все абсолютно прозрачно: нажал на приложение, оно запустилось, выглядит привычно. Но теперь мы полностью контролируем всё — и Task, и Activity. Повторный запуск опять отправит пользователя во вредоносное приложение.
Включаем социальную инженерию
В Android есть возможность давать пользователю самому выбрать приложение, которое будет выполнять ту или иную функцию. Выглядит это как всплывающий список иконок приложений с названиями и реализуется при помощи неявных интентов. Попробуем использовать это в своих целях. У этого подхода есть особенность: нам нужно будет заставить пользователя выбрать наше приложение.
Для разнообразия будем атаковать функцию просмотра PDF. Для успешной реализации, а вернее для большего правдоподобия наше вредоносное приложение может действительно иметь возможность просмотра PDF. Реализуем это в отдельной экспортируемой Activity, которая на вид будет соответствовать стилю атакуемого приложения. Название и значок тоже сделаем почти идентичными. В общем, попробуем убедить пользователя, что в приложении есть такая функция, просто реализована она немного непривычным способом.
Пользователь работает с приложением, выбирает PDF, собирается его открыть. Если выбор приложения для просмотра сделан с помощью неявного интента, пользователю будут предложены на выбор программы, которые могут обрабатывать файлы такого типа. Среди них-то и будет прятаться малварь, иконка которой ну очень похожа на иконку того приложения, которое он сейчас использует. Название тоже не должно вызывать подозрений.
Отлично, мы заставили пользователя выбрать наше приложение. AMS запускает вредоносную Activity и кладет на верхушку стека атакуемого приложения. После работы с документом пользователь нажимает на кнопку Back, чтобы вернуться обратно в приложение, но мы не даем ему это сделать. Переопределим метод onBackPressed()
, который отвечает за обработку нажатия на Back. Перепишем его таким образом, чтобы перенаправить пользователя во вредоносное приложение и вывести его экран на Foreground. Готово, ловушка защелкнулась!
Защищаемся от удаления
Теперь нужно как-то закрепиться в системе. Первое, что приходит в голову, — это применить тот же самый принцип, чтобы защитить малварь от удаления. Ведь когда мы заходим в настройки Android или на экран удаления приложений, мы просто запускаем отдельные системные приложения со своим taskAffinity, которые ничем не отличаются от остальных, разве что иногда выполняются с большими привилегиями. Сказано — сделано, приступаем!
По старой памяти запускаем в Background Task с taskAffinity приложения настроек. В одном случае из-за дефекта с недокументированным поведением AMS приложение настроек просто не запустится. Такой вариант совершенно не интересен — рано или поздно поведение AMS придет в норму. Рассмотрим лучше случай, когда сервис работает как положено.
Итак, все работает как и должно, а в Background ждет своего часа условно вредоносный Task. Запускаем приложение настроек. AMS запускает Activity, находит наш Task и кладет его на верхушку стека. Теперь настройки живут в таске, который мы контролируем. Пользователь переходит по экранам, заглядывает в удаление приложений, видит там зловреда и нажимает на кнопку «Удалить».
После нажатия Android показывает диалоговое окно, которое предлагает подтвердить намерение удалить приложение. Это диалоговое окно не что иное, как очередная Activity, которую мы можем перекрыть своей, как только она появится внутри нашего таска. Делаем похожий экран с неактивной кнопкой «Удалить». При этом, как уже делали ранее, переписываем метод onBackPressed()
таким образом, что при нажатии на Back снова запускаем Mal-Activity 1 в Background, используя флаг FLAG\_CLEAR\_TASK
, чтобы очистить содержимое таска. И мы снова возвращаемся к изначальному состоянию.
Некоторые продвинутые пользователи могут попробовать удалить нехорошее приложение через Android Device Bridge (ADB). Но для того, чтобы удалось подключить устройство к компьютеру, необходимо включить опцию «Отладка по USB». Включение этой опции тоже находится в настройках... Дальнейшее развитие событий очевидно.
Лайфхаки
Автозапуск
Когда пользователь сам запускает вредоносное приложение — это удача для атакующего. Но что, если оно сможет запускаться самостоятельно? Для этого в приложении можно объявить BroadcastReceiver
, который система будет запускать при загрузке. Например, пишем в AndroidManifest.xml
:
<receiver android:name=".BootReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<!--need for working run after reboot on Samsung, HTC devices-->
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
<intent-filter>
<!--need for working run after reboot on MIUI devices-->
<action android:name="android.intent.action.REBOOT" />
</intent-filter>
</receiver>
Можно привязаться и к другим событиям, например изменению состояния подключения к интернету, получению SMS. Правда, в современных версиях Android для работы автозапуска приложения требуется, чтобы пользователь хотя бы раз запустил его вручную.
FLAG_ACTIVITY_CLEAR_TASK
Как ранее упоминалось, флаг FLAG\_ACTIVITY\_NEW\_TASK
желательно использовать совместно с FLAG\_ACTIVITY\_CLEAR_TASK
для того, чтобы очищать содержимое стека Task запускаемой Activity. Но этот же флаг может использоваться злоумышленником, чтобы гарантированно «занять» Task с целевым taskAffinity
.
Intent activityIntent = new Intent(this, MainActivity.class);
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(activityIntent);
Это может быть полезно в случае, если целевое приложение уже запускалось и злоумышленнику требуется заменить содержимое его Task на свое. Например, при автозапуске после получения SMS.
dumpsys
Чтобы получить список запущенных в системе тасков, содержимое их бэкстека и информацию о его Activity, можно воспользоваться следующей командой:
$ adb shell dumpsys activity activities
Она выдаст вот такую структуру данных AMS:
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
Stack #1:
Task id #2586
...
* TaskRecord{c5ccc28 #2586 A=com.android.vending U=0 StackId=1 sz=1}
...
affinity=com.android.vending
intent={flg=0x10800000 cmp=ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity}
realActivity=ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity
...
* Hist #0: ActivityRecord{4b83936 u0 ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity t2586}
packageName=ru.mobsec.calcapp.CTFMobileBank.malware processName=ru.mobsec.calcapp.CTFMobileBank.malware
launchedFromUid=10140 launchedFromPackage=ru.mobsec.calcapp.CTFMobileBank.malware userId=0
app=ProcessRecord{4e9bf32 15729:ru.mobsec.calcapp.CTFMobileBank.malware/u0a140}
Intent { flg=0x10808000 cmp=ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity bnds=[878,1265][1046,1433] (has extras) }
frontOfTask=true task=TaskRecord{c5ccc28 #2586 A=com.android.vending U=0 StackId=1 sz=1}
taskAffinity=com.android.vending
realActivity=ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity
Для наглядности приведена только часть информации. Из вывода видно, что на момент запуска в Foreground находится Task c taskAffinity=com.android.vending (Play Маркет)
и realActivity=ru.mobsec.calcapp.CTFMobileBank.malware/ru.mobsec.calcapp.MainActivity
. Суди сам, к чему это может привести пользователя.
Немного статистики
Авторы первоначального доклада собрали внушительную статистику, проанализировав большое количество приложений на предмет уязвимости к атакам подобного рода. Мы немного дополнили эту статистику.
Тип уязвимости | Процент уязвимых приложений |
---|---|
Подмена приложений из Launcher | 100% |
Использование неявных интентов | 93,9% |
Использование FLAG_ACTIVITY_NEW_TASK | 65,5% |
Использование режима запуска singleTask | 14,2% |
Сейчас перед такими атаками уязвимы все версии Android начиная с 3.x. Уязвимость никак не зависит от производителя устройства. Подмена приложения из Launcher при помощи указания taskAffinity не работает только в CyanogenMod. Очевидно, код запуска приложений в нем сильно изменен по сравнению с официальным вариантом. Тем не менее остальные способы работают.
Как защититься?
Рассмотрим основные способы борьбы с подобным вариантом атаки на приложения. Проблема связана со стандартным Android API, поэтому единственная возможная защита — это поменьше использовать уязвимые методы.
- С осторожностью использовать
FLAG\_ACTIVITY\_NEW_TASK
, лучше вообще не делать этого без необходимости. Если же все-таки без него обойтись не получится, использовать его вместе с флагомFLAG\_ACTIVITY\_CLEAR\_TASK
. Перед созданием Activity он очистит Task, к которому она будет привязана, обнулит его содержимое и сделает твою Activity корневой (нижней в стеке). - Не указывать режим запуска singleTask для Activity приложения. Если есть необходимость запустить в отдельном таске, то использовать рекомендацию из пункта 1.
- Не указывать для Activity атрибут
allowTaskReparenting
, чтобы предотвратить перемещение Activity на стек вредоносного таска. - Использовать явные интенты для вызова сторонних приложений. Либо использовать неявные интенты с применением белого списка приложений, разрешенных к запуску. Как вариант, можно проверять такие приложения по цифровой подписи.
К сожалению, полноценного решения, позволяющего полностью защититься от атак подобного рода, не существует. Есть несколько идей, как потенциально можно предотвратить возможность атаки. Это создание сервиса, который бы отслеживал наличие в системе Task с taskAffinity
защищаемого приложения. Когда такой таск будет найден, можно сверить цифровую подпись породившего его приложения и в случае несоответствия выдавать предупреждение или не запускать приложение. В рамках такого сервиса еще можно реализовать проверку таска, в котором создается Activity приложения.
Выводы?
Два года. Два года, Карл! Думается, что проблема до сих пор не решена потому, что уязвимость эта скорее архитектурная. Такие возможности управления задачами призваны помочь разработчикам и создать единое пространство, в котором работает приложение. Разумеется, никакие объяснения не будут достаточны для нас, хакеров, программистов, да и просто пользователей — ведь мы должны знать, как себя обезопасить. Если не себя, то хотя бы системные приложения!
INFO
Если программа устанавливает себе taskAffinity
приложения настроек или любого другого системного приложения, то это явно нестандартное поведение.
В качестве временной меры разработчикам могли бы дать возможность запрещать другим приложениям производить какие-либо манипуляции со своим Task. Например, пусть это будет новый атрибут в манифесте приложения, который бы определял, может ли другая программа иметь тот же taskAffinity
. Если значение выставлено в false, то система будет игнорировать попытки вредоносных приложений пересадить Activity на свой Task или перекрыть ее при помощи allowTaskReparenting
.
Хочется верить, что в ближайшем будущем мы увидим исправление и официальные рекомендации по защите от подмены приложений таким способом. Пока что нам остается только с осторожностью использовать механизм многозадачности или писать костыли в виде сервисов, мониторящих состояние системы.