这是CVE-2021-39749的PoC,它允许在Android 12L Beta上无视其他应用的permission和exported设置启动其Activity
在Android 12L中,TaskFragmentOrganizer的访问(有意地)不再需要MANAGE_ACTIVITY_TASKS权限
使用此处提供的应用需要禁用Hidden API Checks,你可以通过adb shell settings put global hidden_api_policy 1来实现。这些不是安全边界,并且存在已知的基于应用的绕过方法
以下是修复此漏洞(以及原始报告中提到的几个相关问题)的提交:
startActivityInTaskFragment不再依赖Binder.getCallingUid()ResolverActivity现在启用了relinquishTaskIdentityTaskFragment的SurfaceControl不再提供ActivityRecord#appToken发送给TaskFragmentOrganizer现在基于uid而非pid你可以检出android-12.1.0_r4,还原前3个提交(或前2个,应用仍然能够关闭设备(通过启动ShutdownActivity),但“缩放并设置透明度”复选框将无法工作)
(如果你尝试还原该列表中的第一个提交,测试中会出现合并冲突,但你可以忽略这些)
Binder.getCallingUid()Binder.getCallingUid()方法返回发送当前正在处理的Binder事务的进程的uid。该uid存储在线程局部变量中。处理事务的代码可以调用Binder.clearCallingIdentity()将该变量设置为自身进程的uid,以指示在事务处理期间稍后调用的方法应针对自身(处理事务的代码)而非Binder事务的调用者进行权限检查
有时存在总是在Binder.clearCallingIdentity()之后调用的Binder.getCallingUid(),因此总是返回自身进程的uid。有时这是有意为之,例如在ActivityTaskManagerService#startDreamActivity中(尽管这是执行Process.myUid()或Os.getuid()的一种相当迂回的方式)
我为自己编写了一个(基于Soot的)静态分析工具,用于报告此类只能在Binder.clearCallingIdentity()之后发生的Binder.getCallingUid()调用(以及其他权限检查)。(我有处理Soot提供的Jimple/Shimple IR的自定义逻辑,尽管使用Soot可能有更好的方法,但这是我目前拥有的)
在Android 12L Beta中,该工具在ActivityStartController#startActivityInTaskFragment中发现了一个(注意:当时源代码不可用,因为Beta版本不是开源的,但Shimple通常可读,因此我也将Soot用作Java反编译器)
startActivityInTaskFragment作为静态分析报告的一部分,我获得了从onTransact()实现(Binder调用开始处)到startActivityInTaskFragment的调用层次结构:
IWindowOrganizerController的aidl生成代码中的onTransactWindowOrganizerController#applyTransaction(不带CallerInfo参数)WindowOrganizerController#applyTransaction(带CallerInfo参数)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragment我发现TaskFragmentOrganizer类中存在对applyTransaction的Binder调用,我决定将其用作比直接执行所有Binder调用更方便的包装器(两者都不是公共API,因此无论如何我都必须使用反射)
首先,方法“2.”调用enforceTaskPermission,在Android 12.0上检查仅signature级别的MANAGE_ACTIVITY_TASKS权限,我们无法获得该权限,然而在Android 12L上规则被放宽,某些事务可以在没有权限的情况下进行。事实证明,执行startActivityInTaskFragment所需的操作都不需要权限(如果事务关联了TaskFragmentOrganizer)
因此我们想要执行HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT。为此,我们必须将我们的TaskFragment注册在mLaunchTaskFragments中,否则将报告"Not allowed to operate with invalid fragment token"异常
我们可以通过HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT注册这样的TaskFragment,它会调用createTaskFragment()
(在PoC代码中,这些事务在SecondActivity中发送:HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT由initOrganizerAndFragment()发送,HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT由startActivityInOrganizer发送)
因此这允许我们调用startActivityInTaskFragment,并且此处启动的Activity的Intent被视为来自系统uid,但事实证明这本身并不能让我们做任何事情:系统启动的Activity不能进行URI授权,如果我们尝试启动另一个应用的Activity,我们将被canEmbedActivity检查阻止
canEmbedActivity让我们再次查看canEmbedActivity:如果taskFragment.getTask().effectiveUid是系统uid或与启动的应用的uid匹配,则允许嵌入。我们需要处于effectiveUid为系统的任务中
再回到createTaskFragment():仅当rootActivity.getUid() != ownerActivity.getUid()时才允许创建TaskFragment。这意味着我们的Activity需要位于其所在Task的返回栈底部
我们需要启动一个新的Task(通过Intent.FLAG_ACTIVITY_NEW_TASK),该Task将包含属于系统uid的Activity(因此Task#effectiveUid将被设置为AID_SYSTEM),然后该Activity将启动我们的Activity(在同一Task内)并finish()自身(因此我们的Activity将成为该任务的根Activity,允许我们使用createTaskFragment())
其中一个这样的Activity是ChooserActivity。Chooser通常用于在用户选择“分享”选项后选择要使用哪个应用。然而ChooserActivity在AndroidManifest.xml中设置了android:relinquishTaskIdentity="true",这意味着当它启动另一个Activity时,它将用新启动的应用的uid覆盖Task#effectiveUid
(relinquishTaskIdentity仅在由Task中的第一个应用使用时有效,且仅对系统应用有效,因此我们不能自己使用relinquishTaskIdentity并启动系统应用来覆盖我们Task的Task#effectiveUid)
另一个这样的Activity(可以启动我们的Activity并finish()自身)是ResolverActivity。它在启动解析为多个Activity的隐式Intent时使用。Resolver(与Chooser不同)确实提供记住选择的选项,这就是你(作为手机用户)区分两者的方式。ResolverActivity没有设置relinquishTaskIdentity,然而Resolver使用自己的Intent来查找可用选项(而Chooser使用Extras中提供的Intent)。这被证明是利用中的一个问题,因为Resolver在启动所选Activity时使用的Intent标志将与用于启动Resolver的标志相同,并且:
Intent.FLAG_ACTIVITY_NEW_TASK,Resolver将在我们的Task内启动,该Task已经永久设置了effectiveUidIntent.FLAG_ACTIVITY_NEW_TASK,Resolver将把选择启动到另一个Task中,然后该Task的effectiveUid将被设置为属于启动的应用解决这些问题的方法是同时使用两者:
ChooserActivity:我们向其Intent提供:
Intent.FLAG_ACTIVITY_NEW_TASK,以便Chooser在新Task中启动(该Task将具有系统的effectiveUid,但仅持续到下一次Activity启动)Intent.EXTRA_INTENT设置为不匹配任何Activity的Intent,Chooser中仅剩的选项将来自Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS包含一个单元素数组:我们希望Chooser启动的Intent(当只有一个选项时,Chooser和Resolver都会跳过提示并立即启动唯一选项并finish()自身)ChooserActivity启动ResolverActivity:
relinquishTaskIdentity,因此现在Task#effectiveUid被设置为系统,并且无论此后在此Task中启动什么Activity都将保持不变(在PoC应用中,这些步骤的准备在FirstActivity中执行)
TaskFragmentOrganizer通过onTaskFragmentAppeared回调接收了一个SurfaceControl,使用该SurfaceControl可以缩放启动的Activity并使其透明,同时它仍将接收触摸事件且不会被视为被遮挡(因此受点击劫持保护的元素仍然可以被点击)
你可以通过勾选PoC应用中的“缩放并设置透明度”复选框来查看
另一件事是传递给TaskFragmentOrganizer回调的TaskFragment内运行的Activity的ActivityRecord#appToken。此列表被过滤为仅包含同一进程内Activity的令牌,但检查是通过比较TaskFragmentOrganizer的pid与我们可以获取其appToken的Activity的pid来完成的。我实际上没有检查过,但我认为应用可以创建TaskFragmentOrganizer,退出最初用于创建它的进程,并让其pid被另一个应用的Activity的pid重用,以获取其appToken。这是将验证从基于pid切换为基于uid的提交(上面修复列表中的“4.”)(看起来这个提交是独立于我的报告完成的(尽管在其之后))
一旦攻击者获得Activity的appToken,他们就可以注入onActivityResult()调用(即使目标应用自己没有调用startActivityForResult()),并可能篡改savedInstanceState(通过调用activityStopped(),假设攻击者能在目标应用调用该方法的竞态中获胜,并且额外的调用不会因崩溃导致状态丢失)
我没有检查在这种情况下是否可行,然而之前,利用CVE-2020-0001(是的,我得到了一个花哨的编号),我能够使用savedInstanceState篡改和onActivityResult()注入来欺骗系统设置应用在无需用户交互的情况下启用我的AccessibilityService,但那是另一个故事了
Intent.FLAG_ACTIVITY_NEW_TASK,因此下一个Activity在同一Task内启动<intent-filter>,因此Resolver立即继续启动我们的ActivityResolverActivity启动我们的Activity
effectiveUid为AID_SYSTEM的Task中,因此canEmbedActivity()允许任何操作finish()自身,因此我们是Task中的根Activity,并且被允许使用createTaskFragment()