Вы когда-нибудь копировали в буфер обмена уязвимую информацию, например, пароли, номера кредитных карт, сообщения или личные данные? Если да, эти данные могут оставаться в буфере устройства достаточно длительное время. Доверяете ли вы буферу обмена и приложениям, получающим доступ к этим данным? В этой статье мы изучим Android Clipboard Manager и продемонстрируем необходимость более качественной защиты копируемых данных.

Как устроен доступ к данным буфера обмена


В Android последний скопированный элемент сохраняется в основной клип. Любое приложение может сохранять текстовую информацию при помощи показанного ниже кода:

val clipboard: ClipboardManager? =
            ContextCompat.getSystemService(this, ClipboardManager::class.java)
val clip = ClipData.newPlainText("", text)
clipboard?.setPrimaryClip(clip)

А вот, как приложение может её считывать:

val clipboard: ClipboardManager? =
            ContextCompat.getSystemService(this, ClipboardManager::class.java)
clipboard.primaryClip?.getItemAt(0)?.text.toString()

Это глобальная переменная с методами get и set, ничего особо сложного. При копировании в буфер обмена данные остаются в нём, пока не будут перезаписаны новым значением или устройство не перезагрузится. Есть ли у этого процесса какие-то ограничения?

▍ До Android 12


Долгое время никаких ограничений не было. До Android 12 приложения могли получать доступ к данным буфера обмена, не уведомляя об этом пользователя, и даже читать их в фоновом режиме, что позволяло рекламным SDK собирать информацию об интересах и действиях пользователям в различных приложениях, особенно если он делился ссылками через буфер обмена.

▍ После Android 12


Эта проблема оставалась актуальной не только для Android, но и для iOS. Сначала Apple предложила если не решение, то хотя бы что-то, позволявшее пользователям осознать проблему. В iOS 14 небольшое окно показывает, что конкретное приложение считывает данные буфера обмена.


В Android 12 Google воспользовалась тем же решением и годом позже добавила похожий механизм: внизу экрана отображается небольшое окошко:


Взглянув на код, можно увидеть отрисовку простого всплывающего уведомления (которое знакомо всем разработчикам под Android):

Binder.withCleanCallingIdentity(() -> {
            try {
                CharSequence callingAppLabel = mPm.getApplicationLabel(
                        mPm.getApplicationInfoAsUser(callingPackage, 0, userId));
                String message =
                        getContext().getString(R.string.pasted_from_clipboard, callingAppLabel);
                Slog.i(TAG, message);
                Toast toastToShow;
                if (SafetyProtectionUtils.shouldShowSafetyProtectionResources(getContext())) {
                    Drawable safetyProtectionIcon = getContext()
                            .getDrawable(R.drawable.ic_safety_protection);
                    toastToShow = Toast.makeCustomToastWithIcon(getContext(),
                            UiThread.get().getLooper(), message,
                            Toast.LENGTH_SHORT, safetyProtectionIcon);
                } else {
                    toastToShow = Toast.makeText(
                            getContext(), UiThread.get().getLooper(), message,
                            Toast.LENGTH_SHORT);
                }
                toastToShow.show();
            } catch (PackageManager.NameNotFoundException e) {
                // do nothing
            }
        });

Также в коде присутствует список исключений для показа уведомления:

if (clipboard.primaryClip == null) {
            return;
        }
        if (Settings.Secure.getInt(getContext().getContentResolver(),
                Settings.Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS,
                (mShowAccessNotifications ? 1 : 0)) == 0) {
            return;
        }
        // Don't notify if the app accessing the clipboard is the same as the current owner.
        if (UserHandle.isSameApp(uid, clipboard.primaryClipUid)) {
            return;
        }
        // Exclude special cases: IME, ContentCapture, Autofill.
        if (isDefaultIme(userId, callingPackage)) {
            return;
        }
        if (mContentCaptureInternal != null
                && mContentCaptureInternal.isContentCaptureServiceForUser(uid, userId)) {
            return;
        }
        if (mAutofillInternal != null
                && mAutofillInternal.isAugmentedAutofillServiceForUser(uid, userId)) {
            return;
        }
        if (mPm.checkPermission(Manifest.permission.SUPPRESS_CLIPBOARD_ACCESS_NOTIFICATION,
                callingPackage) == PackageManager.PERMISSION_GRANTED) {
            return;
        }
        // Don't notify if already notified for this uid and clip.
        if (clipboard.mNotifiedUids.get(uid)) {
            return;
        }

К разрешению Manifest.permission.SUPPRESS_CLIPBOARD_ACCESS_NOTIFICATION имеют доступ только системные приложения. То же самое относится к изменению Settings.Secure.CLIPBOARD_SHOW_ACCESS_NOTIFICATIONS. Однако к системным приложениям в устройствах Android относятся и предустановленные сторонние приложения. То есть эти приложения с лёгкостью могут обойти такую меру защиты.

Эта мера защиты работала в Android и iOS для всех остальных приложений. Сразу после обновления до Android 12 я заметил, что многие мои приложения считывают значение из буфера обмена, но вскоре после этого прекратили это делать.

Значит ли это, что существует какой-то способ обхода данного механизма? На первый взгляд, всё выглядит безопасно: сервис управляет созданием всплывающих уведомлений (toast) и общается с другими приложениями при помощи механизма межпроцессной коммуникации (inter-process communication, IPC) под названием Binder. Отменить или изменить отрисовываемое другим приложением всплывающее уведомление никак нельзя. Но можно ли отрисовать что-нибудь поверх него?

Благодаря системе безопасности Android — это всё-таки невозможно. Все отрисовываемые приложением окна по умолчанию будут находиться под системными окнами. Если только у приложения нет одного разрешения.

Разрешение SYSTEM_ALERT_WINDOW


Разрешение SYSTEM_ALERT_WINDOW позволяет отрисовывать приложения поверх остальных приложений. Вот несколько примеров:

  • Приложения звонков: при поступлении вызова окно приложения звонка отрисовывается поверх всех остальных приложений, чтобы пользователь знал о звонке и мог на него ответить.
  • Значки чатов Facebook*: при появлении в чате нового сообщения всплывает небольшое окно. Оно отрисовывается поверх всего, что есть на экране.
  • Видеосообщения Telegram: кружок отрисовывается поверх экранов приложений и не пропадает при сворачивании приложения; видео продолжает воспроизводиться.

Разрешение необходимо дать в отдельном разделе экрана App Info.


Если задуматься, объяснение выглядит довольно пугающе.

Позволить этому приложению отображаться поверх других используемых приложений. Приложение будет видеть точки, которых вы касаетесь, и изменять отображаемое на экране

Зловредные программы использовали (а может, и продолжают использовать) это разрешение для отрисовки своих экранов поверх запускаемых банковских приложений и кражи учётных данных, введённых пользователем в поддельные окна.

Перерисовывать небольшое всплывающее уведомление при помощи SYSTEM_ALERT_WINDOW — это как палить из пушки по воробьям. Но многие люди используют всплывающие функции мессенджеров наподобие Facebook* и Telegram, а также видеоприложений наподобие YouTube. Давайте проверим, что происходит, если у приложения есть это разрешение, и оно попробует отрисовать что-то поверх исходного всплывающего сообщения.

Сокрытие всплывающего уведомления


При наличии разрешения в целом идея проста: можно использовать LayoutInflater, чтобы выполнить inflate (создание объекта в коде) любого окна. После этого созданное окно передаётся WindowManager (отвечающему за отрисовку окон). Эту задачу выполняет показанный ниже код: он отрисовывает прозрачное окно со структурой, описанной в файле dummy_view.xml.

val windowManager: WindowManager =
            applicationContext.getSystemService(WINDOW_SERVICE) as WindowManager

val params = WindowManager.LayoutParams()
params.format = PixelFormat.TRANSLUCENT
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        params.isFitInsetsIgnoringVisibility = true
}
params.flags = (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)

val view = LayoutInflater.from(applicationContext).inflate(R.layout.dummy_view, null)
windowManager.addView(view, params)

Давайте отрисуем на всплывающем уведомлении небольшой белый прямоугольник.


Несмотря на появление прозрачного белого прямоугольника, сообщение под ним остаётся видимым. Прямоугольник почти прозрачный из-за того, что параметр type WindowManager должен быть как минимум TYPE_APPLICATION_OVERLAY. Это минимальный тип для отрисовки поверх; остальные относятся к системным приложениям и приложениям звонков. В документации говорится следующее: «Система может в любой момент менять позицию, размер или видимость этих окон, чтобы снизить визуальную хаотичность и регулировать использование ресурсов».

Android снова обхитрил нас, сделав окно полупрозрачным. Однако давайте ещё раз подумаем над этой проблемой: у нас есть полупрозрачное окно. Как сделать его менее прозрачным? Очевидное решение — отрисовать поверх него несколько дополнительных окон. Давайте добавим поверх этого окна ещё два белых прямоугольника:


Сработало! Благодаря трём слоям, исходное всплывающее уведомление едва заметно, но при необходимости можно добавить ещё окон. При помощи небольших изменений они могут выглядеть как настоящее всплывающее уведомление. Благодаря Android и его опенсорсному коду это поведение можно быстро воспроизвести:

«Это приложение абсолютно точно не вставляет ничего из буфера обмена»

Поэтому можно перерисовать всплывающее уведомление или другим уведомлением, или любым другим окном. Полное сокрытие исходного уведомления позволяет не сообщать пользователю о действиях с буфером обмена. Любое приложение с разрешением SYSTEM_ALERT_WINDOW может считывать данные из буфера обмена, не уведомляя об этом пользователя. Мы смогли убедиться в этом в самой свежей версии Android 14.

Стоит заметить, что некоторые поставщики уже предприняли меры для решения этой проблемы и не позволяют перерисовывать системные всплывающие уведомления. Например, конкретно этот способ не работает на новых устройствах Samsung с последними версиями One UI. Вы можете проверить, уязвимо ли ваше устройство, запустив представленное ниже демо.

Как запретить приложениям красть данные из буфера обмена в устройстве с Android


  1. Отключите разрешение SYSTEM_ALERT_WINDOW у максимального количества приложений. Несмотря на нашу юмористическую демонстрацию, предоставление этого разрешения может быть опасно, поэтому давайте его только тем приложениям, которым доверяете.
  2. Для защиты данных из буфера обмена не пользуйтесь устройствами с Android 11 и более старыми версиями. По возможности обновитесь до последней версии Android. Если это невозможно, то подумайте об использовании ROM на основе AOSP для Android 12+ (например, Lineage OS), которые применяются для продления срока жизни устройств.
  3. По возможности избегайте копирования уязвимой информации.
  4. Помните, что гиганты рекламного рынка всё равно без вашего ведома могут иметь доступ к данным буфера обмена в Android.

Демо-приложение


Мы подготовили простое приложение, позволяющее увидеть эту методику в действии. Можно использовать его для проверки того, уязвимо ли ваше устройство к этой атаке. Демо предназначено для устройств с Android 12 и выше. Его исходный код открыт и доступен для изучения.


Заключение


При работе с информационной безопасностью (как и с любой другой безопасностью) критически важно найти подходящий баланс между безопасностью и удобством применения. Слишком строгие меры могут отпугнуть пользователей от продукта, и тот же результат может ждать при отсутствии мер защиты. Это касается и буфера обмена. С точки зрения конфиденциальности и безопасности улучшенный механизм в Android должен, как минимум, походить на механизм в браузерах: запускаемый в браузерах код на JavaScript (не их расширения) может иметь доступ к данным буфера обмена только в случае действия, выполняемого пользователем.

Google реализовал в Android простую меру защиты, удерживающую пользователей от перехода на iOS. Он ограничил доступ к считыванию значений из буфера обмена без уведомлений для всех приложений, но оставил его для себя. Доступ компании к буферу обмена позволяет ей иметь ценный источник информации для таргетированной рекламы и сохранения позиции на рынке.

Узнавайте о новых акциях и промокодах первыми из нашего Telegram-канала ????

Комментарии (3)


  1. mazagama
    24.10.2023 05:57
    +3

    Пользуюсь андроидом, наверное, с четверки, но об этой всплывашке узнаю впервые.

    Видимо китайцы в MIUI вырезали её к чертям собачьим, ибо иначе оно висело бы перманентно.


    1. degtiv
      24.10.2023 05:57

      Пользуюсь Samsung s23 ultra, всплывающие уведомления видел пару раз в жизни в адекватных местах, точно не помню где, но вроде при нажатии "поделиться картинкой" она сначала копировалась в клипборд, потом открывался мессенджер, потом происходила вставка картинки из клипборда


  1. iamoblomov
    24.10.2023 05:57

    У майкрософта есть тулза, шарит буфер между андроидом и виндой