Skip to content

PoC for CVE-2021-39749, allowing starting arbitrary Activity on Android 12L Beta

Notifications You must be signed in to change notification settings

michalbednarski/OrganizerTransaction

Repository files navigation

This is PoC for CVE-2021-39749, which allows starting activities of other apps on Android 12L Beta regardless of their permission and exported settings

In Android 12L TaskFragmentOrganizer access (intentionally) no longer requires MANAGE_ACTIVITY_TASKS permission

Using app provided here requires disabling Hidden API Checks, you can do so through adb shell settings put global hidden_api_policy 1. These are not security boundary and there are known app-based bypasses

Here are commits fixing this bug (and few related mentioned in original report):

  1. startActivityInTaskFragment no longer rely on Binder.getCallingUid()
  2. ResolverActivity now has relinquishTaskIdentity enabled
  3. (Not needed for starting other activities, but allows repositioning them around screen and making them transparent and tap-jackable) SurfaceControl of TaskFragment is no longer provided
  4. (Not shown in code here, issue only mentioned in original report) Deciding whenever to send ActivityRecord#appToken to TaskFragmentOrganizer is now based on uid instead of pid

You can checkout android-12.1.0_r4, revert first 3 commits (or first 2, application will still be able shutdown device (by starting ShutdownActivity), but "Zoom and set alpha" checkbox won't work)

(First commit from that list will have merge conflicts in tests if you try to revert it, but you can ignore these)

Binder.getCallingUid() that always returns system uid

Binder.getCallingUid() method returns uid of process that sent currently processed Binder transaction. That uid is stored in thread-local variable. Code handling transaction can call Binder.clearCallingIdentity() to set that variable to uid of own process to indicate to methods called later during transaction handling that permission checks should be done against itself (code handling transaction) and not caller of Binder transaction

Sometimes there are Binder.getCallingUid() that are always called after Binder.clearCallingIdentity(), therefore always return uid of own process. Sometimes this happens intentionally, for example in ActivityTaskManagerService#startDreamActivity (although thats rather convoluted way of doing Process.myUid() or Os.getuid())

I've written for myself a (Soot-based) static analysis tool that reports such Binder.getCallingUid() calls (and other permission checks) that can only happen after Binder.clearCallingIdentity(). (I have custom logic handling Jimple/Shimple IR provided by Soot, although there might be better way to do so with Soot, but thats what I have now)

In Android 12L Beta that tool found one in ActivityStartController#startActivityInTaskFragment (Note: source code was not available then as Beta releases are not open source, but Shimple is generally readable so I've been using Soot also as Java decompiler)

How to call startActivityInTaskFragment

As part of static analysis report I've got call hierarchy from onTransact() implementation (where Binder call starts) to startActivityInTaskFragment:

  1. onTransact in aidl-generated code of IWindowOrganizerController
  2. WindowOrganizerController#applyTransaction (without CallerInfo argument)
  3. WindowOrganizerController#applyTransaction (with CallerInfo argument)
  4. WindowOrganizerController#applyHierarchyOp
  5. ActivityStartController#startActivityInTaskFragment

I've found that Binder calls to applyTransaction are present in TaskFragmentOrganizer class and I've decided to use it as more convenient wrapper than doing all Binder calls directly (neither is public API so I had to use reflection anyway)

First of all, method "2." calls enforceTaskPermission, which on Android 12.0 checked signature-only MANAGE_ACTIVITY_TASKS permission which we couldn't get, however on Android 12L rules were relaxed so certain transactions can be made without permissions. It turned out that none of operations needed for doing startActivityInTaskFragment required a permission (if transaction had TaskFragmentOrganizer associated)

So we want to perform HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT. In order to do so we must have our TaskFragment registered in mLaunchTaskFragments otherwise "Not allowed to operate with invalid fragment token" exception will be reported

We can register such TaskFragment through HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, which calls createTaskFragment()

(In PoC code these transactions are sent in SecondActivity: HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT is sent by initOrganizerAndFragment() and HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT is sent by startActivityInOrganizer)

So that allows us to call startActivityInTaskFragment and Intents of Activities started here are considered to be coming from system uid, but it turns out that in itself it doesn't let us do anything: activities started by system cannot do URI grants and if we try to launch activity of another app we'll be stopped by canEmbedActivity check

Bypassing canEmbedActivity

Lets take a look at canEmbedActivity again: embedding is allowed if taskFragment.getTask().effectiveUid is uid of system or matches uid of launched app. We'll need to be in task whose effectiveUid is system

Also step back to createTaskFragment(): creation of TaskFragment was only allowed if rootActivity.getUid() != ownerActivity.getUid(). This means that our activity will need to be at bottom of back-stack of Task it is in

We'll need to launch new Task (through Intent.FLAG_ACTIVITY_NEW_TASK) which will have Activity belonging to system uid (so Task#effectiveUid will be set to AID_SYSTEM) and then that Activity will start our Activity (within same Task) and finish() itself (so our Activity will become root of that task allowing us to use createTaskFragment())

One of such Activities is ChooserActivity. Chooser is usually used for choosing to which app user wants to use after selecting "share" option. ChooserActivity however does have android:relinquishTaskIdentity="true" set in AndroidManifest.xml, which means that when it launches another Activity it will overwrite Task#effectiveUid with uid of newly-launched app

(relinquishTaskIdentity only works when used by first app in Task and only for system apps, so we cannot use relinquishTaskIdentity ourselves and launch system app to overwrite Task#effectiveUid of our Task)

Another such Activity (that can start our Activity and finish() itself) is ResolverActivity. It is used when starting implicit Intent that resolves to multiple Activities. Resolver (unlike Chooser) does offer option to remember choice, which is how you (as user of phone) can distinguish those two. ResolverActivity did not have relinquishTaskIdentity set, however Resolver uses own Intent to find what options are available (while Chooser takes Intent provided in Extras). This turns out to be a problem for exploitation because Intent flags used by Resolver when launching selected Activity will be same as those used to launch Resolver and:

  • If we don't set Intent.FLAG_ACTIVITY_NEW_TASK, Resolver will be launched within our Task which already has effectiveUid permanently set
  • If we do set Intent.FLAG_ACTIVITY_NEW_TASK, Resolver will launch selection into yet another Task, which will then have effectiveUid set to one belonging to launched app

The solution to these problems is to use both:

  1. First we launch ChooserActivity: We provide to its Intent:
    • Intent.FLAG_ACTIVITY_NEW_TASK, so Chooser is launched in new Task (which will have effectiveUid of system but only until next Activity launch)
    • Intent.EXTRA_INTENT set to an Intent which doesn't match any Activities and only options left in Chooser will come from Intent.EXTRA_INITIAL_INTENTS
    • Intent.EXTRA_INITIAL_INTENTS containing array with one-element: The Intent we want Chooser to launch (when there is only one option both Chooser and Resolver skip prompt and immediately launch only option and finish() itself)
  2. Then ResolverActivity is launched by ChooserActivity:
    • Resolver didn't have relinquishTaskIdentity set, so now Task#effectiveUid is set to system and will stay that way regardless of next Activities launched in this Task
    • Intent does not have Intent.FLAG_ACTIVITY_NEW_TASK, so next Activity is launched within same Task
    • Intent action is set to non-standard one, matching only <intent-filter> we declared ourselves in our app, so Resolver immediately proceeds to launching our Activity
  3. ResolverActivity launches our Activity
    • Now we're in Task whose effectiveUid is AID_SYSTEM, so canEmbedActivity() allows anything
    • Both Chooser and Resolver have finished themselves, so we're root Activity in Task and are allowed to use createTaskFragment()

(In PoC app preparation of these steps is performed in FirstActivity)

Other tricks with TaskFragmentOrganizer

TaskFragmentOrganizer received a SurfaceControl through onTaskFragmentAppeared callback and using that SurfaceControl one can scale launched Activity and make it transparent, while it will still receive touch events and won't be considered obscured (so tap-jacking-protected elements can still be tapped)

You can see that by checking "Zoom and set alpha" checkbox in PoC app

This is fixed by commit "3." from fixes list at top


Another thing is that ActivityRecord#appToken-s of Activities running inside TaskFragment passed to TaskFragmentOrganizer callbacks. This list was filtered to only include tokens of Activities within same process, however check was done by comparing pid of TaskFragmentOrganizer with pid of Activity whose appToken we could get. I haven't actually checked but I think application could create TaskFragmentOrganizer, exit process used to initially create it and have its pid reused as pid of Activity of another app in order to get its appToken. Here's commit ("4." in fixes list above) that switches verification from pid-based to uid-based (it looks like this commit was done independently of my report though (although after it))

Once attacker gets appToken of an Activity they can inject onActivityResult() calls (even if target app didn't call startActivityForResult() themselves) and possibly tamper savedInstanceState (by calling activityStopped(), assuming attacker can win race with target application calling that method and additional call won't cause state to be lost due to crash)

I haven't checked if that can be done in this case, however previously, with CVE-2020-0001 (Yay, I've got fancy number), I was able to use savedInstanceState tampering and onActivityResult() injection to trick system settings app into enabling my AccessibilityService without user interaction, but that is a story for another time

About

PoC for CVE-2021-39749, allowing starting arbitrary Activity on Android 12L Beta

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages