Как стать автором
Обновить
43.23
Deiteriy Lab
Делаем пентесты

Не подсматривай: защищаем данные пользователей от скриншотов

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров2.7K

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

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

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

Механизмы защиты в Android

Для начала давайте рассмотрим какие меры противодействия снятию информации есть в Android.

FLAG_SECURE

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

... getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); ...

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

Рассмотрим работу этого механизма защиты на примере простого приложения с формой для ввода критичных данных. Без установки атрибута FLAG_SECURE запись экрана выполняется без каких-либо проблем:

Приложение с отключенной защитой
Приложение с отключенной защитой

Но, при попытке записи экрана в приложении с реализованным механизмом защиты, мы увидим следующее:

Приложение с включенной защитой
Приложение с включенной защитой

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

android:inputType=”textPassword”

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

Например, для полей, в которых вводятся платежные данные (CVC2/CVV2), можно использовать атрибут android:inputType="textPassword" для того, чтобы скрыть введенные символы.

<EditText
    android:id="@+id/passwordField"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="CVV"
    android:inputType="textPassword" />

RenderEffect

Экран могут снять не только в режиме использования, но и в фоновом режиме, поэтому рекомендуется скрывать критичные данные при отображении приложения в списке "недавние" или при переходе в фоновый режим. Для этого можно использовать размытие (blur) содержимого с помощью RenderEffect, однако данный API доступен только с Android версии 12.

Для Android более старших версий можно использовать RenderScript.

private void applyBlurEffect() {
    RenderEffect blurEffect = RenderEffect.createBlurEffect(40f, 40f, Shader.TileMode.CLAMP);
    rootView.setRenderEffect(blurEffect);
}

40f, 40f - это радиус размытия по горизонтали (X) и вертикали (Y). Значение определяет насколько сильно будет размыто изображение: чем больше значение, тем шире размываются пиксели по горизонтали и вертикали соответственно.

Вот так выглядит размытие страницы приложения в списке недавних приложений.

Размытие страницы приложения
Размытие страницы приложения

Механизмы защиты в iOS

В отличие от Android, в iOS нет готового решения, чтобы запретить пользователям делать снимки экрана или осуществлять его запись. Однако Apple позволяет отслеживать факт записи экрана и реагировать на это событие.

UIScreen.main.isCaptured

Для этого можно использовать свойство UIScreen.main.isCaptured, которое возвращает true, если экран в данный момент записывается или транслируется, и false в противном случае. Например:

if UIScreen.main.isCaptured {
    print("Экран записывается!")
}

Кроме того, можно подписаться на UIScreen.capturedDidChangeNotification, чтобы отслеживать изменение статуса захвата экрана в режиме реального времени:

NotificationCenter.default.addObserver(forName: UIScreen.capturedDidChangeNotification,
                                       object: nil,
                                       queue: .main) { _ in
    if UIScreen.main.isCaptured {
        print("Началась запись экрана!")
    } else {
        print("Запись экрана остановлена.")
    }
}

Однако, согласно документации Apple isCaptured находится в статусе deprecated и его поддержка прекращается, начиная с iOS 18.4. Поэтому вместо isCaptured следует использовать sceneCaptureState, доступную с iOS 17.0.

Кроме того, в iOS можно оповещать пользователя, если был сделан снимок экрана. Это можно сделать с помощью UIApplication.userDidTakeScreenshotNotification:

NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification,
                                       object: nil,
                                       queue: .main) { _ in
    print("Пользователь сделал снимок экрана!")
}

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

Получается, что “коробочных” средств для предотвращения скриншотов в iOS нет. Но мы не отчаивались и продолжили поиски.

Скрытие контента от захвата экрана с помощью isSecureTextEntry

Мы нашли интересную статью на эту тему в блоге Кирилла Сидорова. В ней он описывал, как искал решение данной проблемы, изучая работу Telegram. Ссылка на оригинальную статью: https://sidorov.tech/all/lovim-skrinshoty/.

Основной механизм защиты, описанный в статье, основан на системной функции UITextField.isSecureTextEntry, которая изначально предназначена для скрытия контента (например, паролей) при вводе. При активации этого флага, iOS автоматически предотвращает попадание реального текста в системные буферы рендеринга, что защищает данные от скриншотов и записи экрана.

В примере ниже создается специальный слой, который временно заменяет слой View, и с помощью флага isSecureTextEntry заставляет UITextField перерисовать его содержимое, например, скрыв текст.

public extension CALayer {
    func makeHiddenOnCapture() {
    // Поиск специального системного View для защиты
        let captureSecuredView: UIView? = captureSecuredView
            ?? uiKitTextField.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("LayoutCanvasView") })
            
        // Сохраняем оригинальный слой и подменяем его текущим слоем
        let originalLayer = captureSecuredView?.layer
        captureSecuredView?.setValue(self, forKey: "layer")
        
        // Активируем и деактивируем secureTextEntry для триггера защиты
        uiKitTextField.isSecureTextEntry = false
        uiKitTextField.isSecureTextEntry = true
        
        // Возвращаем оригинальный слой
        captureSecuredView?.setValue(originalLayer, forKey: "layer")
    }
}

Как работает этот код:

  1. Сначала находим нужный внутренний слой, который будет временно заменен.

  2. Заменяем этот слой на новый, который будет скрывать содержимое.

  3. С помощью переключения значения isSecureTextEntry для UITextField перерисовываем содержимое, скрывая текст (например, пароль или номер карты). Стоит отметить, что isSecureTextEntry для TextField устанавливает атрибут disableUpdateMask, который отправляется на рендеринг (Подробнее о disableUpdateMask).

  4. После того как процесс перерисовки завершен, восстанавливается оригинальный слой.

Напишем интеграцию UIKitView для использования в SwiftUI с возможностью скрытия от скриншотов и записи.

// Представляем UIView внутри SwiftUI, позволяя использовать UIKit-функциональность
struct HiddenOnCaptureColorView: UIViewRepresentable {
    let color: UIColor
    
    // Создаем и возвращаем UIView для отображения в SwiftUI
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        
    // Применяем защиту от захвата экрана к слою View
        view.layer.makeHiddenOnCapture()
        updateViewColor(view: view)
        return view
    }
  
    // Обновление View при изменении данных
    func updateUIView(_ uiView: UIView, context: Context) {
        updateViewColor(view: uiView)
    }
    
   func updateViewColor(view: UIView) {
        view.backgroundColor = color
    }
}

Как работает этот фрагмент кода:

  1. Создает обертку UIViewRepresentable для использования UIKit View в SwiftUI.

  2. При создании View применяет защиту от захвата экрана к его слою.

  3. Обновляет цвет фона View при изменениях.

  4. Служит мостом между UIKit и SwiftUI для реализации функции защиты.

Далее добавим модификатор-маску для скрытия контента.

// Модификатор для скрытия контента при захвате экрана (в обычном режиме контент виден)
public struct HiddenOnCaptureModifier: ViewModifier {
    public func body(content: Content) -> some View {
        content.mask {
        // Создаем маску на основе яркости
            ZStack {
                Color.black // Будет полностью прозрачным (яркость → 0% alpha)
                HiddenOnCaptureColorView(color: .white) // Будет видимым (яркость → 100% alpha) с защитой
            }
            .compositingGroup() // Обрабатываем стек как единое изображение перед преобразованием
            .luminanceToAlpha() // Преобразуем яркость в прозрачность (0% яркости = прозрачно, 100% = непрозрачно)
        }
    }
}

Как работает этот фрагмент кода:

  1. Создает модификатор для SwiftUI, который можно применять к любым View.

  2. Использует технику маскирования через преобразование яркости в прозрачность.

  3. compositingGroup() гарантирует, что преобразование применяется к объединенному результату ZStack.

  4. luminanceToAlpha() выполняет ключевую работу:

    • Анализирует яркость каждого пикселя;

    • Преобразует значение яркости в значение прозрачности;

    • Создает маску, где только защищенный View остается видимым.

Добавляем для удобства расширения View, чтобы можно было использовать модификатор .hiddenOnCapture во View.

// Расширение для View, добавляющее модификатор скрытия контента при захвате экрана
public extension View {
    func hiddenOnCapture() -> some View {
        modifier(HiddenOnCaptureModifier())
    }
}

Как работает этот фрагмент кода:

  1. Добавляет метод hiddenOnCapture() ко всем SwiftUI View.

  2. Позволяет легко применять защиту вызовом .hiddenOnCapture().

  3. Упрощает использование защиты без необходимости помнить детали реализации.

Далее остается только добавить ContentView, и применить к нему описанные ранее модификаторы для подключения реализованной защиты.


struct ContentView: View {
...

    var body: some View {
        VStack {
            Text("Hello, World!")
 ...
 
        .hiddenOnCapture()
    }
}
}

Рассмотрим, как ведет себя приложение без использования защиты:

Приложение с отключенной защитой
Приложение с отключенной защитой

Как мы видим - данные в открытом виде попадают на запись экрана. Добавим .hiddenOnCapture() и посмотрим, как данные будут скрываться на записи.

Приложение с включенной защитой
Приложение с включенной защитой

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

Использование модификаторов для скрытия критичной информации в фоновом режиме

Для того, чтобы скрыть критичную информацию при отображении приложения в списке "недавние" или при переходе в фоновый режим, можно написать кастомный модификатор, например:

// Модификатор, который добавляет размытие и затемнение View при переходе приложения в фоновый режим
struct AppBlurModifier: ViewModifier {
    // Локальное состояние, определяющее, должно ли содержимое быть размытым
    @State private var isBlurred = false

    // Основное тело модификатора, которое применяет изменения к View
    func body(content: Content) -> some View {
        ZStack {
            content
                .blur(radius: isBlurred ? 10 : 0)
                .animation(.easeInOut(duration: 0.2), value: isBlurred)


            // Наложение полупрозрачного черного цвета при активном размытии
            if isBlurred {
                Color.black.opacity(0.4)
            }
        }
        // Подписка на уведомление о переходе приложения в фоновый режим
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
            isBlurred = true
        }
        
        // Подписка на уведомление о возврате приложения в активный режим
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
            isBlurred = false
        }
    }
}

// Расширение для View, добавляющее удобный метод применения модификатора
extension View {
    func blurWhenBackgrounded() -> some View {
        self.modifier(AppBlurModifier())
    }
}

В дальнейшем наш модификатор можно применить ко всему View:

View {
	VStack {
    // UI, в котором отображаются критичные данные
	}
	.blurWhenBackgrounded() // кастомный модификатор
}

Для маскирования критичных данных, таких, как пароли или CVV в iOS, можно использовать компонент SecureField. Он работает аналогично обычному TextField, но скрывает вводимые символы, заменяя их точками или звёздочками.

Что в итоге

Используя описанные выше техники, можно защитить приложения от захвата конфиденциальной информации через скриншоты и запись экрана.

Внедрение этих механизмов защиты может предотвратить возможную утечку критичных пользовательских данных через вредоносное ПО.

Теги:
Хабы:
+1
Комментарии11

Публикации

Информация

Сайт
lab.deiteriy.com
Дата регистрации
Дата основания
Численность
31–50 человек
Местоположение
Россия
Представитель
Антон

Истории