Screen Sharing Security Settings Monitoring

      Technique summary
    Technique Screen sharing security settings monitoring
    Against Screen recording attacks
    Limitations Android ≥15
    Side effects None
    Recommendations Can be used for applications running on Android 15 and newer

    Android 15 provides built-in protection against recording the contents of password fields. Under normal circumstances, the protection works automatically. However, it can be explicitly disabled by the user through Developer options (Disable screen share protections switch) or by malware that has root privileges on the device.

    Users can be tricked into disabling protection, for example using social engineering attacks. This technique will detect when the built-in protection is disabled.

    This code snippet defines a list of settings to monitor. In this example, it will monitor “show taps” and “disable screen share protections”.

    public class SecuritySettingsManager { private static SecuritySettingsManager instance; private final Map<String, SecuritySetting> settings; private SecuritySettingsManager() { settings = new HashMap<>(); SecuritySetting setting; setting = new SecuritySetting( "disable_screen_share_protections_for_apps_and_notifications", "Disable screen share protections", 0, SecuritySetting.SettingNamespace.GLOBAL); settings.put(setting.getId(), setting); setting = new SecuritySetting( "show_touches", "Show taps", 0, SecuritySetting.SettingNamespace.SYSTEM); settings.put(setting.getId(), setting); } public static SecuritySettingsManager getInstance() { if (instance == null) { instance = new SecuritySettingsManager(); } return instance; } public SecuritySetting getSetting(String settingId) { return settings.get(settingId); } public int getSize() { return settings.size(); } public Set<String> getSettingsIds() { return settings.keySet(); } } object SecuritySettingsManager { val settings: Map<String, SecuritySetting> = HashMap<String, SecuritySetting>().apply { val disableScreenShareProtectionsId = "disable_screen_share_protections_for_apps_and_notifications" put( disableScreenShareProtectionsId, SecuritySetting( disableScreenShareProtectionsId, "Disable screen share protections", 0, SecuritySetting.SettingNamespace.GLOBAL ) ) val showTouchesId = "show_touches" put( showTouchesId, SecuritySetting( showTouchesId, "Show taps", 0, SecuritySetting.SettingNamespace.SYSTEM ) ) } }

    This next snippet will register a callback that will be executed whenever there is a change on any setting from a particular namespace (e.g., global, system). If the setting was one from the list and it has been changed into an insecure state, a reaction will be triggered (e.g., a toast showing a message to the user).

    public class SettingsChangeObserver extends ContentObserver { private static final String TAG = "SettingsObserver"; private final ProtectedActivity mActivity; public SettingsChangeObserver(Handler handler, ProtectedActivity activity) { super(handler); mActivity = activity; } @Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); SecuritySettingsManager settingsManager = SecuritySettingsManager.getInstance(); Set<String> settingsIds = settingsManager.getSettingsIds(); SecuritySetting setting; int state; Uri settingUri; String dangerousState; for (String settingId : settingsIds) { setting = settingsManager.getSetting(settingId); if (setting.getNamespace() == SecuritySetting.SettingNamespace.SYSTEM) { settingUri = Settings.System.getUriFor(settingId); } else if (setting.getNamespace() == SecuritySetting.SettingNamespace.GLOBAL) { settingUri = Settings.Global.getUriFor(settingId); } else { throw new RuntimeException( "[!] Invalid setting namespace: " + setting.getNamespace()); } if (settingUri.equals(uri)) { setting = settingsManager.getSetting(settingId); try { if (setting.getNamespace() == SecuritySetting.SettingNamespace.SYSTEM) { state = Settings.System.getInt( mActivity.getContentResolver(), settingId); } else if (setting.getNamespace() == SecuritySetting.SettingNamespace.GLOBAL) { state = Settings.Global.getInt( mActivity.getContentResolver(), settingId); } else { throw new RuntimeException( "[!] Invalid setting namespace: " + setting.getNamespace()); } } catch (Settings.SettingNotFoundException e) { Log.e(TAG, "[!] Failed to retrieve state for setting " + setting); continue; } Log.d(TAG, settingId + " changed to " + state); if(state != setting.getSecureState()) { Log.d(TAG, "[!] Setting '" + settingId + "' has an insecure value! --> " + state); dangerousState = setting.getSecureState() == 0 ? "enabled" : "disabled"; mActivity.showMessageOnToast(String.format( "%s is %s!!", setting.getName(), dangerousState)); } break; } } } } class SettingsChangeObserver(handler: Handler?, private val mActivity: ProtectedActivity) : ContentObserver(handler) { override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) var state: Int var settingUri: Uri val dangerousState: String for ((settingId, setting) in SecuritySettingsManager.settings) { settingUri = when (setting.namespace) { SettingNamespace.SYSTEM -> Settings.System.getUriFor(settingId) SettingNamespace.GLOBAL -> Settings.Global.getUriFor(settingId) } if (settingUri == uri) { state = when(setting.namespace) { SettingNamespace.SYSTEM -> Settings.System.getInt( mActivity.contentResolver, settingId ) SettingNamespace.GLOBAL -> Settings.Global.getInt( mActivity.contentResolver, settingId ) } Log.d( TAG, "$settingId changed to $state" ) if (state != setting.secureState) { Log.d( TAG, "[!] Setting '" + settingId + "' has an insecure value! --> " + state ) dangerousState = if (setting.secureState == 0) "enabled" else "disabled" mActivity.showMessageOnToast( String.format( "%s is %s!!", setting.name, dangerousState ) ) } break } } } companion object { private const val TAG = "SettingsObserver" } }

    This snippet defines a thread that would check the settings state periodically.

    public class SettingsChecker implements Runnable { private static final String TAG = "SettingsChecker"; private final ProtectedActivity activity; private final Context context; private final int intervalLength; // milliseconds public SettingsChecker(ProtectedActivity activity, int intervalLength) { this.activity = activity; this.context = activity.getApplicationContext(); this.intervalLength = intervalLength; } @Override public void run() { int state; SecuritySettingsManager settingsManager = SecuritySettingsManager.getInstance(); Set<String> settingsIds = settingsManager.getSettingsIds(); SecuritySetting setting; String dangerousState; while(true) { for (String settingId : settingsIds) { setting = settingsManager.getSetting(settingId); try { if (setting.getNamespace() == SecuritySetting.SettingNamespace.SYSTEM) { state = Settings.System.getInt( context.getContentResolver(), settingId); } else if (setting.getNamespace() == SecuritySetting.SettingNamespace.GLOBAL) { state = Settings.Global.getInt( context.getContentResolver(), settingId); } else { throw new RuntimeException( "[!] Invalid setting namespace: " + setting.getNamespace()); } } catch (Settings.SettingNotFoundException e) { Log.e(TAG, "[!] Failed to retrieve state for setting " + setting); continue; } if(state != setting.getSecureState()) { Log.d(TAG, "[!] Setting '" + settingId + "' has an insecure value! --> " + state); dangerousState = setting.getSecureState() == 0 ? "enabled" : "disabled"; activity.showMessageOnToast(String.format( "%s is %s!!", setting.getName(), dangerousState)); } } try { Thread.sleep(intervalLength); } catch (InterruptedException e) { Log.d(TAG, "[!] Interrupted exception: " + e.getMessage()); } } } } class SettingsChecker( private val activity: ProtectedActivity, private val intervalLength: Int // milliseconds ) : Runnable { private val context: Context = activity.applicationContext override fun run() { while (true) { for ((settingId, setting) in SecuritySettingsManager.settings) { val state = when(setting.namespace) { SettingNamespace.SYSTEM -> Settings.System.getInt( context.contentResolver, settingId ) SettingNamespace.GLOBAL -> Settings.Global.getInt( context.contentResolver, settingId ) } if (state != setting.secureState) { Log.d( TAG, "[!] Setting '" + settingId + "' has an insecure value! --> " + state ) val dangerousState = if (setting.secureState == 0) "enabled" else "disabled" activity.showMessageOnToast( String.format( "%s is %s!!", setting.name, dangerousState ) ) } } try { Thread.sleep(intervalLength.toLong()) } catch (e: InterruptedException) { Log.d(TAG, "[!] Interrupted exception: " + e.message) } } } companion object { private const val TAG = "SettingsChecker" } }

    And the final snippet binds all the pieces together to run the verification checks.

    public void enableSettingsProtection(Context context, int intervalLength) { // Setting up the observer SettingsChangeObserver settingsChangeObserver = new SettingsChangeObserver(new Handler(), mActivity); SecuritySettingsManager settingsManager = SecuritySettingsManager.getInstance(); Set<String> settingsIds = settingsManager.getSettingsIds(); SecuritySetting setting; for (String settingId : settingsIds) { setting = settingsManager.getSetting(settingId); if (setting.getNamespace() == SecuritySetting.SettingNamespace.SYSTEM) { context.getContentResolver().registerContentObserver( Settings.System.getUriFor(settingId), true, settingsChangeObserver); } else if (setting.getNamespace() == SecuritySetting.SettingNamespace.GLOBAL) { context.getContentResolver().registerContentObserver( Settings.Global.getUriFor(settingId), true, settingsChangeObserver); } else { throw new RuntimeException( "[!] Invalid setting namespace: " + setting.getNamespace()); } } // Setting up the thread to check new Thread(new SettingsChecker(mActivity, intervalLength)).start(); } fun enableSettingsProtection(context: Context, intervalLength: Int) { // Setting up the observer val settingsChangeObserver = SettingsChangeObserver(Handler(), mActivity) for ((settingId, setting) in SecuritySettingsManager.settings) { when (setting.namespace) { SYSTEM -> context.contentResolver.registerContentObserver( Settings.System.getUriFor(settingId), true, settingsChangeObserver ) GLOBAL -> context.contentResolver.registerContentObserver( Settings.Global.getUriFor(settingId), true, settingsChangeObserver ) } } // Setting up the thread to check Thread(SettingsChecker(mActivity, intervalLength)).start() }

    Guardsquare

    Table of contents