|
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()
}