User leave tracking

      Technique summary
    Technique User leave tracking
    Against UI injections: Activity injections
    Limitations Does not detect view injections
    Side effects Causes a false positive when the user initiates app switching by tapping on a system UI element, such as a notification or the quick settings
    Recommendations Recommended for use combined with other techniques and with reaction logic that accommodates for false positives

    An algorithm to detect an activity injection consists of two parts:

    1. Detect that the protected activity has been deactivated
    2. Filter out the cases when the protected activity was deactivated intentionally

    The user leave tracking, or ULT technique defines intentional as:

    • The user has pressed a button or performed a gesture to switch away from the protected activity, or
    • The application where the protected activity is has initiated opening of another internal or external activity.

    Deactivation detection

    To detect that the protected activity was deactivated, ULT uses the onPause() method override. 

    Sometimes the reason for the activity switch is immediately clear (for example, if the protected application initiated the launch of a different activity). In this case, the decision to allow the activity switch can be made immediately.

    However, in other cases, the reason cannot be determined on the spot. For example, when the activity switch was initiated by the user clicking the Home button, some time may pass before the protected application receives a broadcast message. For the latter case, onPause() implementation must launch a thread that would detect valid reasons to switch after the activity was paused.

    Intentional switch detection

    To detect that the protected activity was switched out intentionally, a number of events have to be intercepted:

    • Android buttons:
      • For the Back button, override the onBackPressed() method.
      • For the Home and Recents buttons, register a broadcast receiver.
    • Activity launch from the protected application:
      • Intercept every instance of the protected application starting activities. For example, using ActivityMonitor (see Android documentation).

    In general, it is not possible to determine whether the activity switch was done as a direct consequence of the user’s or program’s action.  Instead, ULT will attribute the switch to the action if it happened shortly after the switch. To do this, you can use a regular timer.

    Check algorithm

    graph TD reason[Deactivation Reason] minimize([Minimize the app]) transition([Transition to different activity]) back{Back to previous activity?} app_transfer([App-initiated transition to another app]) sys_transfer([System UI-initiated transition]) attack([Activity injection attack]) home_recent_button[BroadcastReceived for Home/Recent apps actions] back_button[onBackPressed override] activity_monitor[ActivityMonitor tracking] allow[Allow] detect[Trigger a detection] fp[Known false positive] style allow fill:lightgreen style detect fill:red style fp fill:yellow style minimize fill:lightgrey style transition fill:lightgrey style app_transfer fill:lightgrey style sys_transfer fill:lightgrey style attack fill:lightgrey reason --> minimize reason --> transition reason --> app_transfer reason --> sys_transfer reason --> attack transition --> back minimize --> home_recent_button --> allow back -- Yes --> back_button --> allow back -- No --> activity_monitor --> allow app_transfer --> activity_monitor --> allow sys_transfer --> fp attack --> detect

    Code highlights

    The explanation here omits many of the implementation details in order to better highlight the general approach of the check. We address the important parts of the implementation but exclude the more mechanical details for the sake of brevity.

    Handle OnPause() to detect activity deactivation:

    // Pseudocode to illustrate the logic of the onPause() implementation @Override public void onPause() { super.onPause(); new Thread() { @Override public void run() { // Wait to allow all broadcast messages to come through Thread.sleep(MESSAGE_TIMEOUT); // Trigger a detection if the switch was not intentional int switchDelay = currentTime() - this.intentionalSwitchTime; if (Math.abs(switchDelay) > SWITCH_THRESHOLD) { triggerDetection(); } } }.start(); }

    Handle buttons to detect intentional switch:

    // Pseudocode to illustrate the logic of handling buttons @Override public void onBackPressed() { super.onBackPressed(); this.intentionalSwitchTime = currentTime(); } ... private static class HomeAndRecentsListener extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (!Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) { return; } String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY); if (reason == null) { return; } if (!reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS) && !reason.equals(SYSTEM_DIALOG_REASON_HOME_KEY) && !reason.equals(SYSTEM_DIALOG_REASON_DREAM)) { return; } activity.intentionalSwitchTime = currentTime(); } }

    Add activity monitoring:

    Class<?> clz = Class.forName("android.app.ActivityThread"); Object currentThread = clz.getMethod("currentActivityThread").invoke(null); Instrumentation instrumentation = (Instrumentation)clz.getMethod("getInstrumentation").invoke(currentThread); MyActivityMonitor monitor = new MyActivityMonitor(); instrumentation.addMonitor(monitor);

     

    Note

    False positives may occur if the user takes advantage of system UI (such as notifications) to switch between apps.

    As the action on detection is a soft block (such as a warning message or bringing the obscured activity back on top), having a false positive does not entail severe consequences.

    Further reading

    See Android documentation on:

    Guardsquare

    Table of contents