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):
startActivityInTaskFragment
no longer rely onBinder.getCallingUid()
ResolverActivity
now hasrelinquishTaskIdentity
enabled- (Not needed for starting other activities, but allows repositioning them around screen and making them transparent and tap-jackable)
SurfaceControl
ofTaskFragment
is no longer provided - (Not shown in code here, issue only mentioned in original report) Deciding whenever to send
ActivityRecord#appToken
toTaskFragmentOrganizer
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()
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)
As part of static analysis report I've got call hierarchy from onTransact()
implementation (where Binder call starts) to startActivityInTaskFragment
:
onTransact
in aidl-generated code ofIWindowOrganizerController
WindowOrganizerController#applyTransaction
(withoutCallerInfo
argument)WindowOrganizerController#applyTransaction
(withCallerInfo
argument)WindowOrganizerController#applyHierarchyOp
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
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 ourTask
which already haseffectiveUid
permanently set - If we do set
Intent.FLAG_ACTIVITY_NEW_TASK
, Resolver will launch selection into yet anotherTask
, which will then haveeffectiveUid
set to one belonging to launched app
The solution to these problems is to use both:
- First we launch
ChooserActivity
: We provide to its Intent:Intent.FLAG_ACTIVITY_NEW_TASK
, so Chooser is launched in new Task (which will haveeffectiveUid
of system but only until next Activity launch)Intent.EXTRA_INTENT
set to anIntent
which doesn't match any Activities and only options left in Chooser will come fromIntent.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 andfinish()
itself)
- Then
ResolverActivity
is launched byChooserActivity
:- Resolver didn't have
relinquishTaskIdentity
set, so nowTask#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
- Resolver didn't have
ResolverActivity
launches our Activity- Now we're in Task whose
effectiveUid
isAID_SYSTEM
, socanEmbedActivity()
allows anything - Both Chooser and Resolver have finished themselves, so we're root Activity in Task and are allowed to use
createTaskFragment()
- Now we're in Task whose
(In PoC app preparation of these steps is performed in FirstActivity
)
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