При обсуждении защиты чувствительных экранов в Android обычно имеют в виду механизмы защищённого вывода изображения. На практике разработчик чаще всего сталкивается с WindowManager.LayoutParams.FLAG_SECURE и близкими средствами платформы, которые запрещают создание обычных скриншотов, ограничивают вывод окна на небезопасные внешние дисплеи и в ряде сценариев затрудняют захват содержимого экрана системными средствами. Для отдельных поверхностей аналогичная защита может применяться и на уровне SurfaceView. Такой слой важен для экранов, где отображаются платёжные данные, одноразовые коды, персональная информация и другие чувствительные сведения.

Экран, защищенный FLAG_SECURE, не будет показан при трансляции экрана или при отображении миниатюры в менеджере задач, как на скриншоте.
При этом возможности защищённого вывода не следует переоценивать. Они уменьшают риск утечки изображения экрана как визуального представления, но сами по себе не решают проблему чтения структурированного интерфейса через специальные инструменты. Иными словами, защита изображения и защита семантики интерфейса относятся к разным уровням. Первая препятствует захвату экранного содержимого как картинки, тогда как вторая должна не допустить передачи текста, описаний, состояний и дерева элементов внешнему потребителю.
Почему AccessibilityService стал проблемой безопасности
В Android есть специальный системный механизм под названием AccessibilityService. Он нужен для того, чтобы одни приложения могли помогать пользователю взаимодействовать с другими: озвучивать содержимое экрана, упрощать навигацию, поддерживать альтернативный ввод и делать интерфейс доступнее для людей с ограниченными возможностями.
Для такой помощи системе недостаточно просто «видеть картинку» на экране. AccessibilityService получает более глубокий доступ к интерфейсу: может узнавать, какие окна открыты, какие элементы на них расположены, какой у них текст, описание, состояние и какие действия выполняет пользователь.
Именно здесь и появляется проблема безопасности. Механизм, задуманный как инструмент доступности, при неправильном использовании превращается в удобный способ наблюдать за действиями пользователя и считывать содержимое интерфейса. По сути, приложение с таким доступом получает не снимок экрана, а его структурированное представление.
Если приложение добилось доступа к accessibility, оно может:
читать содержимое интерфейса;
отслеживать смену экранов;
видеть ввод и действия пользователя;
нажимать кнопки и выполнять жесты от имени пользователя;
мешать удалению себя и проводить жертву по нужным экранам настроек.
Из-за этого accessibility уже много лет используется мобильным вредоносным ПО, в основном ратниками (Remote access trojan), как удобный «пульт управления» устройством. Особенно часто это встречается у банковских троянов: через такой доступ они читают интерфейс банковских приложений, перехватывают коды, навязывают действия и скрывают свою активность.
Как вредоносное ПО получает такой доступ
Обычно схема довольно приземлённая: социальная инженерия.
Пользователю показывают, что для «правильной работы» приложения нужно выдать дополнительные разрешения. Дальше его буквально доводят до нужного экрана: подсказками, оверлеями, фальшивыми объяснениями, иногда давлением в духе «без этого приложение не заработает». То есть главный барьер здесь не технический, а поведенческий. Пользователя убеждают включить опасную возможность.
На практике получение AccessibilityService-доступа даёт вредоносному приложению не один, а сразу несколько каналов наблюдения за интерфейсом. Во-первых, сервис получает события пользовательского интерфейса: сведения о смене окон, изменении текста в полях, перемещении фокуса, появлении новых активностей и других значимых действиях. Уже этого достаточно, чтобы реконструировать сценарий работы пользователя почти в реальном времени: определить, какое приложение открыто, на каком экране находится жертва и в какой момент начинается ввод чувствительных данных. Во-вторых, при доступе к содержимому окна сервис может читать дерево элементов интерфейса. В таком представлении злоумышленнику становятся доступны текстовые значения, contentDescription, структура экрана, а в ряде случаев и идентификаторы элементов. В результате атакующий получает не просто изображение экрана, а достаточно точную семантическую модель интерфейса, пригодную для автоматизированного анализа и навигации. Именно сочетание событийного канала и дерева элементов делает AccessibilityService особенно опасным: он позволяет не только извлекать данные, но и синхронизировать вредоносную логику с текущими действиями пользователя, что создаёт техническую основу для удалённого сопровождения банковской сессии, on-device fraud и более сложных сценариев скрытого управления устройством.
Современные угрозы, основанные на злоупотреблении AccessibilityService, подробно рассматриваются как в аналитических обзорах, так и в разборах конкретных семейств мобильного вредоносного ПО. В русскоязычных материалах ESET показано, что использование службы специальных возможностей уже давно стало устойчивым приёмом Android-малвари для получения расширенного контроля над устройством и обхода штатных пользовательских сценариев включения опасных разрешений (ESET / BankBot, ESET / DoubleLocker). В публикациях Group-IB / F6 эта проблема рассматривается уже на уровне практической эксплуатации: вредоносное ПО через AccessibilityService отслеживает изменения элементов на экране, имитирует пользовательские действия, мешает открытию защитных настроек и сопровождает мошеннический сценарий до завершения операции (F6 / Gustuff, F6 / Fanta, F6 / Godfather). Дополнительно в русскоязычных обзорах «Лаборатории Касперского» и Securelist подчёркивается, что злоумышленники систематически используют доступ к специальным возможностям для расширения контроля над заражённым устройством, перехвата действий пользователя и противодействия встроенным механизмам защиты Android (Kaspersky / Habr, Securelist)
Например, вот такой пример работы HyperRat можно найти в сети:

Пример тестового экрана и того, как его видит наш «вредоносный» сервис:


Для демонстрации самой идеи, кстати, необязательно писать отдельное приложение. Часть эффекта можно показать средствами ADB и UI Automator. Инструменты этого класса умеют работать с иерархией окна вне процесса тестируемого приложения и позволяют получить XML-представление текущего UI, в котором видны текстовые атрибуты, resource-id, contentDescription, классы и границы элементов.
При этом здесь важно аккуратно обозначить границы метода. ADB и UI Automator – это исследовательские и тестовые инструменты, а не точная модель вредоносного ПО. Они требуют включённой отладки или тестовой среды и не воспроизводят весь жизненный цикл злоупотребления AccessibilityService. Тем не менее для демонстрации утечки дерева элементов их вполне достаточно: команда вида
adb exec-out uiautomator dump /dev/tty > view.xml
позволяет получить дамп текущей иерархии окна, а затем наглядно сравнить его с результатом после включения защиты.
Ниже приведен пример вывода команды для нашего экрана до включения защиты.

Из чего вообще состоит защита
Защита от чтения экрана через accessibility в Android не решается одной настройкой. На практике это набор отдельных мер, и каждая закрывает свой канал утечки или управления.
1. Убираем чувствительные куски из дерева доступности
Первый слой – не отдавать лишнее в accessibility вообще .Для обычных View это можно делать на уровне контейнера: помечать его так, чтобы он и его дочерние элементы не считались важными для accessibility.
2. Чистим то, что всё-таки может уйти наружу
Но одного скрытия дерева мало. Часть данных может утекать через события и описание узлов. Поэтому второй слой — перехватывать и чистить этот payload: тексты, описания, свойства, действия.
Для этого в платформе есть удобная точка расширения — View.AccessibilityDelegate. Через него можно влиять на то, как View заполняет AccessibilityEvent и AccessibilityNodeInfo, то есть обрезать полезную нагрузку ещё до того, как она станет полезной внешнему сервису.
3. Помечаем чувствительные данные на уровне самой системы
Новый и очень полезный слой – accessibilityDataSensitive. Смысл в том, что разработчик явно говорит системе: этот View или composable содержит чувствительные данные. После этого доступ к нему не получают приложения, которые запросили accessibility-доступ, но не являются легитимными accessibility-инструментами. При этом для настоящих assistive-сервисов сценарий не ломается. Google отдельно пишет, что попытка «притвориться» таким инструментом заканчивается отклонением в Play и блокировкой через Play Protect.
4. Закрываем побочные каналы вывода экрана
Следующий слой – защита уже не от чтения дерева, а от банального вывода картинки экрана. Здесь работает FLAG_SECURE: он запрещает скриншоты и не даёт показывать окно на небезопасных внешних дисплеях. Это полезный барьер, особенно для экранов логина, платежей и других чувствительных сценариев. Но важно понимать его границы: он уменьшает риск утечки через снимки экрана и шэринг, но не решает все UI-атаки сам по себе.
5. Блокируем overlay-атаки и tapjacking
Отдельная история – атаки через перекрытие интерфейса. Когда поверх экрана рисуют overlay и вынуждают пользователя нажать не туда, куда он думает.
Для такого случая в Android есть setFilterTouchesWhenObscured(true). Этот механизм блокирует касания, которые пришли через overlay. Плюс в свежих изменениях Android эта защита стала ещё полезнее: платформа связала её с защитой чувствительных accessibility-данных, так что один защитный слой начал усиливать другой.
6. Для Compose всё то же самое, только через semantics
В Jetpack Compose основная часть данных для accessibility живёт не в классическом дереве View, а в semantics tree. Именно semantics даёт контекст сервисам доступности, автозаполнению и тестам. Если нужно жёстко убрать данные, используется clearAndSetSemantics. Причём пустая лямбда – это самый жёсткий вариант: очищенные semantics после этого вообще не отправляются потребителям вроде accessibility, autofill и testing.
А так вредоносная система видит все тот же экран после очистки:

Ну и заодно проверим вывод UI Automator:

Боремся с угрозами на практике
Для тестов мы собрали маленькое опенсорсное решение, где защиту мождно включить одним переключателем (ведь на деле лишать сервис специальных возможностей глаз – это личное дело пользователя)
В нашей библиотеке защита собрана по слоям: отдельно закрываем вывод экрана, отдельно режем дерево View, отдельно чистим AccessibilityEvent и AccessibilityNodeInfo, отдельно помечаем чувствительные участки и отдельно закрываем семантику в Compose. Такой подход нужен потому, что у Android несколько каналов утечки сразу, и каждый приходится перекрывать отдельно. FLAG_SECURE закрывает скриншоты и небезопасные дисплеи, importantForAccessibility = NO_HIDE_DESCENDANTS убирает поддерево из accessibility-модели, View.AccessibilityDelegate даёт точку для очистки событий, а setFilterTouchesWhenObscured(true) блокирует касания через overlay. Для чувствительных экранов Android также поддерживает accessibilityDataSensitive для View и composable.
1. Закрываем окно
Сначала убираем самый дешёвый канал утечки: скриншоты и показ окна на небезопасных дисплеях.
import android.view.Window import android.view.WindowManager
fun secureWindow(window: Window, enabled: Boolean) {
if (enabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
// в этом месте добавили запрет на скриншоты и показ окна на небезопасных дисплеях
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
// в этом месте вернули обычное поведение окна
}
}
FLAG_SECURE полезен, но сам по себе он не решает задачу чтения интерфейса через accessibility и не защищает от overlay-атак. Поэтому дальше идём в само дерево UI.
2. Убираем чувствительный экран из accessibility-дерева
Если экран действительно чувствительный, мы не оставляем его в дереве доступности целиком.
import android.os.Buildimport android.view.View
fun secureRoot(root: View, enabled: Boolean) {
root.importantForAccessibility = if (enabled) {
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
}
// в этом месте убрали root и всё его поддерево из accessibility-дерева
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
root.importantForAutofill = if (enabled) {
View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS
} else {
View.IMPORTANT_FOR_AUTOFILL_AUTO
}
// в этом месте выключили autofill для root и всех потомков
}
}
Если сервис доступности не видит поддерево, ему уже нечего удобно обходить. Параллельно мы убираем экран из сервиса автозаполнения, чтобы чувствительные поля не попадали в ещё один системный канал разбора формы.
3. Чистим то, что всё же может уйти наружу
Даже если поддерево скрыто, часть данных всё равно может уходить через события и остаточную информацию в узлах. Поэтому следующий слой – собственный AccessibilityDelegate, который рекурсивно ставится на всё дерево.
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProviderfun installScrubbingDelegate(root: View) {
applyRecursively(root) { view ->
view.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {
scrubEvent(event)
// в этом месте очистили событие перед отправкой наружу
}
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {
super.onInitializeAccessibilityEvent(host, event)
scrubEvent(event)
event.className = View::class.java.name
// в этом месте убрали читаемые поля из AccessibilityEvent
}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(host, info)
scrubNode(info)
// в этом месте очистили AccessibilityNodeInfo
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
return false
// в этом месте запретили выполнять accessibility-action на защищённом узле
}
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? {
return null
// в этом месте не отдаём наружу виртуальное дерево
}
}
}
}
private fun scrubEvent(event: AccessibilityEvent) {
event.text.clear()
event.contentDescription = null
}
private fun scrubNode(info: AccessibilityNodeInfo) {
info.text = null
info.contentDescription = null
info.className = View::class.java.name
info.isClickable = false
info.isLongClickable = false
info.isFocusable = false
info.isScrollable = false
if (Build.VERSION.SDK_INT >= 26) {
info.hintText = null
}
if (Build.VERSION.SDK_INT >= 30) {
info.stateDescription = null
}
if (Build.VERSION.SDK_INT >= 18) {
info.viewIdResourceName = null
}
if (Build.VERSION.SDK_INT >= 21) {
val actions = info.actionList.toList()
actions.forEach(info::removeAction)
}
if (Build.VERSION.SDK_INT >= 19) {
info.extras.clear()
}
}
private inline fun applyRecursively(view: View, block: (View) -> Unit) {
block(view)
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
applyRecursively(view.getChildAt(i), block)
}
}
}
Это главный слой для View: даже если сервис получил событие или узел, он получает пустую оболочку без текста, подсказок, id и действий. То есть защита здесь строится не на сокрытии пикселей, а на обнулении полезной нагрузки. AccessibilityDelegate как раз и нужен для того, чтобы перехватить инициализацию событий, узлов и действий в одной точке.
4. Помечаем экран как чувствительный и режем overlay
Для чувствительных экранов мы не только чистим дерево, но и явно помечаем View как sensitive. Одновременно включаем защиту от tapjacking.
import android.os.Build
import android.view.Viewfun markSensitiveView(view: View) {
view.filterTouchesWhenObscured = true
// в этом месте заблокировали касания, прошедшие через overlay
if (Build.VERSION.SDK_INT >= 34) {
view.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
// в этом месте пометили View как чувствительный для accessibility
}
if (Build.VERSION.SDK_INT >= 26) {
view.setAutofillHints()
// в этом месте убрали autofill hints у чувствительного поля
}
}
setFilterTouchesWhenObscured(true) закрывает касания через перекрывающий слой. На новых версиях Android эта защита дополнительно увязана с accessibilityDataSensitive, так что один включённый механизм сразу усиливает другой. А accessibilityDataSensitive нужен для того, чтобы обычное приложение с accessibility-доступом не могло читать такие View и взаимодействовать с ними как с обычными узлами.
Что делаем отдельно для Compose
В Compose проблема другая: там важен не только host View, но и semantics tree. Поэтому в библиотеке защита для Compose идёт в двух местах сразу.
5. Защищаем host View у Compose
Даже если экран написан на Compose, под ним всё равно есть обычный Android View. Его мы защищаем теми же системными API.
import android.os.Build
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalView@Composable
fun SecureComposeHost(content: @Composable () -> Unit) {
val composeView = LocalView.current
DisposableEffect(composeView) {
composeView.filterTouchesWhenObscured = true
// в этом месте включили защиту host View от tapjacking
if (Build.VERSION.SDK_INT >= 34) {
composeView.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
// в этом месте пометили host View как чувствительный
}
onDispose {
composeView.filterTouchesWhenObscured = false
}
}
content()
}
Это закрывает системный уровень: overlay и чувствительность самого контейнера Compose. Для Compose такой слой нужен обязательно, потому что он всё равно живёт внутри View. Google прямо показывает такой способ для Compose через LocalView.current и рекомендует помечать чувствительные composable через sensitiveData.
6. Помечаем конкретный composable как чувствительный
Если экран не надо “гасить” целиком, а нужно пометить конкретный блок, используем semantics.
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.sensitiveData@Composable
fun SensitiveText(value: String) {
Text(
text = value,
modifier = Modifier.semantics {
sensitiveData = true
// в этом месте пометили composable как чувствительный
}
)
}
7. Полностью вычищаем semantics там, где экран нельзя отдавать вообще
Для самых жёстких сценариев мы используем полную очистку semantics – удаление семантической модели для всех внешних потребителей. Такой слой надо включать только на действительно критичных блоках: PIN, одноразовый код, платёжный экран, чувствительные персональные данные.
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.clearAndSetSemantics
@Composable
fun FullyHiddenSemantics(content: @Composable () -> Unit) {
Box(
modifier = Modifier.clearAndSetSemantics {
// в этом месте полностью очистили semantics для accessibility, autofill и тестов
}
) {
content()
}
}
Чтобы не собирать такую защиту вручную из набора разрозненных API, мы вынесли её в отдельную библиотеку secure-ui (https://github.com/infohexteam/AndroidSecuredScreen). Она закрывает основные практические каналы утечки для чувствительных экранов: может включать FLAG_SECURE, скрывать accessibility-дерево, очищать AccessibilityEvent и AccessibilityNodeInfo, помечать View как sensitive на Android 14+, ограничивать assist и autofill, а также включать filterTouchesWhenObscured. В репозитории также есть отдельный модуль attacker, который наглядно показывает, какие данные внешний AccessibilityService действительно видит до включения защиты и что от них остаётся после.
С практической точки зрения идея максимально простая: библиотеку нужно подключить к проекту и добавить несколько вызовов в чувствительные экраны. Для XML/View-экранов достаточно применить политику к корневому View, например через rootView.applySecurePolicy(...), либо подписать экран на общий режим через rootView.bindSecurityMode(...). Для Compose-экранов нужно применить host policy через ApplySecureComposeHostPolicy() и добавить Modifier.secureSemantics(...) на контейнер с чувствительным содержимым. После этого переключение режима можно централизованно выполнять через SecurityModeStore, не размазывая защитную логику по всему приложению.
Больше подробностей можно найти в readme.md репозитория.
Что мы имеем в итоге: конечно, доступность приложения для людей с ограниченными возможностями очень важна. Крупные компании выпускают статьи и делают доклады, как они добавили озвучивание всех кнопок и всего текста в очередном обновлении. Но нужно не забывать и о безопасности пользователей и хотя бы давать им возможность защищать чувствительные данные от потенциальных мошеннических приложений.