Содержание статьи
- Что такое экстренные службы в Android
- Микрофон, который включается сам
- SensorPrivacyService и колбэк экстренного вызова
- AppOpsService: разрешения поверх разрешений
- Прокси-операции и матрешка AttributionSource
- Три судьбы операции
- Находка: resume без разбора
- План атаки
- Байпас 1: тег атрибуции произвольной длины
- Байпас 2 — skipProxyOperation и доверенная атрибуция
- Байпас 3 — изолированная служба против SELinux
- Демо: 911 не отвечает
- Что еще сломалось в Personal Safety
- Выводы
Мы пройдем весь путь: от устройства экстренных служб в ОС и внутренностей AppOpsService до трех байпасов и обхода политик SELinux.
warning
Уязвимость давно закрыта, патч приехал на устройства с одним из бюллетеней безопасности Android. Не воспроизводи атаку на устройстве, которым пользуешься как основным, и ни при каких условиях не совершай тестовые звонки на реальные номера экстренных служб — это ложный вызов, и за него предусмотрена ответственность.
Android работает больше чем на трех миллиардах устройств — это порядка 70% всех смартфонов на планете. И на каждом из них есть ровно одна функция, которая обязана отработать при любом раскладе, — вызов экстренных служб.
Google это прекрасно понимает: в матрице оценки серьезности уязвимостей Android есть отдельная строка Targeted prevention of access to emergency services, целенаправленное предотвращение доступа к экстренным службам, и она сразу относит подобное явление к уровню High. При этом таких случаев за всю историю Android набирается меньше десятка.
Одну из подобных уязвимостей мне удалось найти. Она получила идентификатор CVE-2025-22431, категорию High и затронула сразу три мажорные версии Android. Атакующее приложение при этом не запрашивает ни одного разрешения. В этой статье мы разберем, почему при звонке на 911 микрофон включается сам, при чем тут AppOpsService и какие байпасы пришлось сделать, чтобы превратить все это в работающий эксплоит.
Что такое экстренные службы в Android
Для начала краткий обзор — иначе будет непонятно, что именно мы ломаем.
Emergency dialer — это системная звонилка, доступная в том числе с заблокированного экрана. Произвольный номер оттуда не наберешь — только экстренные и те контакты, которые пользователь сам занес в соответствующую категорию. Дозвон при этом работает через любую доступную сеть и без SIM-карты: этого требует стандарт 3GPP TS 22.101. А какой номер считать экстренным, система берет из базы, которая лежит прямо в прошивке, в файле packages/.
Второй интересующий нас компонент — приложение Personal Safety, предустановленное на Pixel. Главный вход в него — Emergency SOS: пятикратное нажатие кнопки питания запускает цепочку действий, которую ты настраиваешь сам.

Из этой цепочки нас интересуют ровно три пункта:
- Record emergency video — экстренная видеозапись, требует камеру и микрофон;
- Emergency sharing — отправка геопозиции доверенным контактам, не работает без включенной геолокации;
- Car Crash Detection — детект ДТП, ждет от тебя голосовую команду, то есть снова нужен микрофон.
Объединяет их одно общее свойство: все три пункта упираются в сенсор, который пользователь мог заранее выключить. И если сенсор заблокирован, то Personal Safety честно попросит его включить — вот таким диалогом.

Вот в этот момент — момент включения сенсора ради экстренной службы — устройство и оказывается уязвимым. Но с системной звонилкой все намного интереснее: там пользователю вообще ничего не нужно нажимать.
Микрофон, который включается сам
Начнем с наблюдения, с которого все завертелось. В Android есть Privacy controls: в шторке быстрых настроек можно заблокировать микрофон и камеру глобально, для всех приложений сразу, невзирая ни на какие выданные разрешения.
Блокируем микрофон, набираем 911 — и звонок спокойно проходит. Более того, если после звонка заглянуть в шторку, то окажется, что микрофон уже не заблокирован: система включила его сама.

Это логично: в экстренной ситуации диспетчер должен тебя слышать и параноидальный тумблер в шторке не должен стоить пользователю жизни. Например, человек мог заблокировать микрофон еще утром и просто забыть об этом. Google, кстати, честно предупреждает юзера прямо в Privacy controls, в описании пункта Microphone access, только мелким шрифтом:
For apps and services. If this setting is off, microphone data may still be shared when you call an emergency number.
Вопрос в том, как именно система это делает, и ответ на него оказался довольно неприятным.
SensorPrivacyService и колбэк экстренного вызова
При загрузке system_server поднимает больше сотни системных служб, и одна из них — SensorPrivacyService. Она управляет состоянием приватности сенсоров, микрофона и камеры, и именно к ней обращается SystemUI, когда ты давишь на тумблер в шторке.
В коде службы есть переопределенный метод onBootPhase, который отрабатывает при загрузке системы. Как можно обнаружить, в нем создается экземпляр приватного внутреннего класса:
com.android.server.sensorprivacy.SensorPrivacyService$CallStateHelper
А в конструкторе хелпера регистрируется колбэк OutgoingEmergencyStateCallback через TelephonyManager#registerTelephonyCallback. Колбэк переопределяет метод onOutgoingEmergencyCall, и внутри вызывается этот метод:
SensorPrivacyService.CallStateHelper#onEmergencyCall
Вот как это выглядит:
frameworks/base/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.javaprivate void onEmergencyCall() { synchronized (mCallStateLock) { if (!mIsInEmergencyCall) { mIsInEmergencyCall = true; if (mSensorPrivacyServiceImpl .isToggleSensorPrivacyEnabled(TOGGLE_TYPE_SOFTWARE, MICROPHONE)) { mSensorPrivacyServiceImpl.setToggleSensorPrivacyUnchecked( TOGGLE_TYPE_SOFTWARE, mCurrentUser, OTHER, MICROPHONE, false); mMicUnmutedForEmergencyCall = true;Как указано в документации TelephonyCallback, метод срабатывает на любой экстренный вызов с любой SIM-картой, включая сценарий без SIM вообще. Код проверяет, заблокирован ли сейчас микрофон, и если да — меняет состояние приватности сенсора. Далее цепочка вызовов приводит нас к приватному методу SensorPrivacyService#setGlobalRestriction:
frameworks/base/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.javaprivate void setGlobalRestriction(int sensor, boolean enabled) { switch(sensor) { case MICROPHONE: mAppOpsManagerInternal.setGlobalRestriction(OP_RECORD_AUDIO, enabled, mAppOpsRestrictionToken); mAppOpsManagerInternal.setGlobalRestriction( OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, enabled, mAppOpsRestrictionToken); mAppOpsManagerInternal.setGlobalRestriction(OP_PHONE_CALL_MICROPHONE, enabled, mAppOpsRestrictionToken); // ...В коде метода видно, что ограничения снимаются сразу для пяти операций, связанных с микрофоном:
OP_RECORD_AUDIO (;op code = 27) OP_PHONE_CALL_MICROPHONE (;100) OP_RECEIVE_AMBIENT_TRIGGER_AUDIO (;120) OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO (;121) OP_RECEIVE_SANDBOX_TRIGGER_AUDIO (.136)
Таким образом, был сделан вывод, что при звонке в службу спасения система снимает ограничения на доступ к микрофону — и снимает их глобально, а не для одной только звонилки. Ключевое слово здесь Global. Соответственно, теперь мы подошли к тому моменту, когда стоит раскрыть роль AppOpsService в системе, чтобы понять, чем именно это грозит нам при звонке из системного dialer’а.
AppOpsService: разрешения поверх разрешений
Даже если ты пишешь софт под Android не первый год, есть большая вероятность, что с AppOpsService ты вживую не сталкивался. Функции этой службы использует чаще всего сама система, а разработчику про нее думать обычно не нужно. Схематично ее место в системе разрешений выглядит так.

У приложения может быть пермишен, а операция при этом запрещена — и доступа оно не получит.
info
AppOpsService — это системная служба, которая работает поверх обычной системы разрешений Android. Даже если пользователь выдал приложению RECORD_AUDIO, служба может запретить конкретную операцию, и приложение не запишет ни секунды звука. Также она ведет централизованный аудит: кто, когда и как долго обращался к микрофону.
Кроме того, каждой чувствительной операции соответствует свой код, и число их растет от релиза к релизу: сейчас в константе _NUM_OP из AppOpsManager содержится 179. Операций при этом заметно больше, чем разрешений: у микрофона один пермишен в манифесте и целых пять разных appops.
Теперь о том, что с операциями можно делать. Работать с ними можно через AppOpsManager: startOp запускает длительную операцию, finishOp завершает ее, noteOp отмечает разовое обращение, checkOp проверяет права. Например, именно startOp зажигает тот самый зеленый кружок в углу экрана.
Прокси-операции и матрешка AttributionSource
А теперь самое интересное. AppOpsService позволяет запускать операции не только напрямую, но и через посредника. Посредник — это приложение, которому доступ к данным сам по себе не нужен, но оно инициирует операцию от лица другого пакета. В документации AppOpsManager это описано как «report that an application has started executing a long-running operation on behalf of another application». Также стоит отметить, что для этого в AppOpsManager имеется отдельная тройка методов — startProxyOp, finishProxyOp и noteProxyOp.
Чтобы служба не путалась, кто кому кем приходится, в системе завели класс AttributionSource. Устроен он как матрешка: сверху всегда прокси, внутри, в поле next, — инициатор, он же proxied package. Каждый уровень несет имя пакета, UID, тег атрибуции и биндер‑токен.
Например, посмотреть на живую прокси‑операцию можно прямо через dumpsys:
$ adb shell dumpsys appops
Package com.example.app:
RECORD_AUDIO (allow):
apkappcontext=[ Access: [fg-tpd] proxy[uid=10164, pkg=com.android.systemui, attributionTag=null] ]Читается это так: доступ к микрофону запросил com., он же proxied package и конечный получатель доступа. А операцию от его имени запустил com. — посредник, которому сам микрофон не нужен.
И вот теперь самое важное для всей атаки: уникальность запущенной операции складывается из пяти параметров — это имя пакета, UID, код операции, device ID (для основного устройства всегда 0) и тег атрибуции. Этот тег показывает, какой именно модуль приложения дернул операцию, и в идеале должен быть зарегистрирован в манифесте приложения с использованием элемента <.
Запомни эту пятерку. Через пару разделов она станет для нас главным инструментом атаки.
Три судьбы операции
Когда приложение просит запустить операцию, служба отвечает одним из режимов: MODE_ALLOWED — можно, MODE_IGNORED — нельзя, MODE_DEFAULT — решай сам. Есть еще MODE_ERRORED и MODE_FOREGROUND, но нам они не понадобятся. Однако возвращаемое значение — это только половина истории, и гораздо важнее то, что при этом происходит внутри самой службы.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
