| |
Technique summary |
| Technique |
Recursive view hiding and restoring |
| Against |
pixel stealing attacks |
| Limitations |
None |
| Side effects |
None |
| Recommendations |
Recommended for use |
In the first steps of the Pixnapping attack, the target activity is launched and a stack of activities is injected on top of it. The defensive technique consists in recursively hiding all the views of our activity as it goes to the background, this frustrates the attack as no information is on the screen to be leaked anymore. The views are made visible again as the activity comes back to the foreground.
In order to implement the protection, we just need to add one instruction in the onPause and onResumemethods of the activity we want to protect:
@Override
protected void onPause() {
super.onPause();
VisibilityManager.hideAllViews(this);
}
@Override
protected void onResume() {
super.onResume();
VisibilityManager.restoreAllViews(this);
}
override fun onPause() {
super.onPause()
VisibilityManager.hideAllViews(this)
}
override fun onResume() {
super.onResume()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
VisibilityManager.hideAllViews(this)
} else {
VisibilityManager.restoreAllViews(this)
}
}
@RequiresApi(Build.VERSION_CODES.Q)
override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) {
super.onTopResumedActivityChanged(isTopResumedActivity)
if (isTopResumedActivity) {
VisibilityManager.restoreAllViews(this)
}
}
For Kotlin code, the approach is slightly different. We learnt from the researchers of an edge case on Android 15 where the onPause event is not reliably delivered on some devices when the launched activity is immediately overlayed. To mitigate this issue, we propose an altered strategy which incorporates the onTopResumedActivity event.
This strategy is based on the observation that the onTopResumedActivityChanged event does not seem to be delivered in the same cases where the onPause event isn’t delivered.
- Under normal launch conditions:
onResume → onTopResumedActivityChanged
- Under attack conditions, when the edge case occurs:
This difference can be used to protect the views. The idea is that, starting from Android 15, onResume will also hide the views, and only the onTopResumedActivityChanged event that should follow it will make them visible again. In case the victim activity is launched normally, the transition from onResume to onTopResumedActivityChanged happens consistently and the views are shown. In case the victim activity is launched and immediately overlayed, the onResume event is received, but the onTopResumedActivityChanged event isn't, leaving the views hidden.
The class that handles view hiding and restoring can be something like this:
public class VisibilityManager {
public static final String TAG = "VisibilityManager";
private static final WeakHashMap>
visibilitiesMap = new WeakHashMap<>();
private static final WeakHashMap
hiddenStateMap = new WeakHashMap<>();
public static void hideAllViews(Activity activity) {
Log.d(TAG, "[*] Hiding all the views");
int hideMode = View.INVISIBLE;
if (activity == null) return;
if (Boolean.TRUE.equals(hiddenStateMap.get(activity))) return;
View root = activity.getWindow().getDecorView();
WeakHashMap originalVisibilities = new WeakHashMap<>();
visibilitiesMap.put(activity, originalVisibilities);
saveAndSetVisibilityRecursive(root, hideMode, originalVisibilities);
hiddenStateMap.put(activity, true);
}
public static void restoreAllViews(Activity activity) {
Log.d(TAG, "[*] Restoring all the views");
if (activity == null) return;
WeakHashMap originalVisibilities =
visibilitiesMap.get(activity);
if (originalVisibilities == null) return;
for (View v : originalVisibilities.keySet()) {
Integer orig = originalVisibilities.get(v);
if (orig != null) v.setVisibility(orig);
}
visibilitiesMap.remove(activity);
hiddenStateMap.put(activity, false);
}
private static void saveAndSetVisibilityRecursive(View view,
int newVisibility,
WeakHashMap visMap) {
if (!visMap.containsKey(view)) {
visMap.put(view, view.getVisibility());
}
view.setVisibility(newVisibility);
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
int childCount = vg.getChildCount();
for (int i = 0; i < childCount; i++) {
saveAndSetVisibilityRecursive(vg.getChildAt(i), newVisibility, visMap);
}
}
}
}