View hiding and restoring

  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:
    • onResume

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

Guardsquare

Table of contents