Intent安全中的一点事儿
Intent是Android程序中不同组件传递数据的一种方式,翻译为意图,Intent既可以用在startActivity方法中来启动Activity,也可以用在sendBroadcast中来发送携带Intent的广播,甚至可以使用startService(Intent) 或者 bindService(Intent, ServiceConnection, int)来和后台Service进行交互。总而言之,Intent是Android不同组件之间交互的一个桥梁,同时也能够在不同的应用之间进行数据的交互。当然,Intent最重要的用途还是在启动Activity时作为粘合剂,下面将会对几种使用Intent的方法作简要介绍。
方法介绍public void startActivityForResult (Intent intent, int requestCode)
这个方法本质上与public void startActivityForResult (Intent intent, int requestCode, Bundle options)相同,用来在启动Activity结束后返回结果,其中第一个参数intent是你需要发送的intent,第二个参数requestCode,当其大于或等于0时,Activity启动后的结果将被归还到onActivityResult方法中;而当其小于0时,该方法本质上就等同于startActivity(Intent),即启动的Activity将不再被作为子Activity,不会返回数据。
public final void setResult (int resultCode, Intent data)
调用此方法可设置Activity返回调用方的result。第一个参数resultCode有三种常量,分别为RESULT_CANCELED(值为0),RESULT_FIRST_USER(值为1)和RESULT_OK(值为-1)。第二个参数Intent在Android 2.3以上版本可以被赋予
Intent.FLAG_GRANT_READ_URI_PERMISSION和/或Intent.FLAG_GRANT_WRITE_URI_PERMISSION 标志。
这将授予接收结果的Activity对Intent中特定 URI 的访问权限,且访问将一直保留到Activity结束。
protected void onActivityResult(int requestCode, int resultCode, Intent data)
这个方法在启动的Activity退出时调用。其中第一个参数requestCode用来提供给onActivityResult,以便确认返回的数据是从哪个Activity返回的,其实就是在startActivityForResult中设置的requestCode。第二个参数resultCode是由子Activity通过其setResult(Int, Intent)方法返回的,值为setResult(int resultCode, Intent data)的第一个参数resultCode。
看完上面枯燥的介绍,相信还是有一点模糊的,光说不练假把式,下面举个例子,说明一下上面这几个函数的实际应用。
在下图所示的Demo中,FirstActivity会通过startActivityForResult方法启动SecondActivity:
SecondActivity收到intent后,会要求用户确认,若确认通过则通过setResult将结果回传FirstActivity:
Intent重定向能够导致三方App越权访问到会导致Android的安全访问限制(应用的沙箱机制)失效,可能会产生以下安全危害:
1)出现LauncherAnyWhere问题
2)通过非导出的 Content Provider组件中的 URI 来窃取敏感文件
3)通过 IntentScheme URL漏洞模型访问任意组件
4)通过PendingIntent越权漏洞模型,劫持和修改Intent来达到提权的目的
5)绕过原有代码执行逻辑
PART3漏洞分析
利用点一:绕过原有代码执行逻辑或敏感数据泄露
第一种情况为,三方应用利用setResult绕过原有的代码执行逻辑或者获取intent中携带的敏感数据。
在上文中,我们能够看见SecondActivity中通过提取FirstActivity中发送的intent,通过进一步处理,再将结果返回,我们查看SecondActivity在AndroidManifest中的定义:
首先,我们需要在三方应用新建一个拥有相同的 intent-filter 的Activity,这样三方应用就能够接收到startActivityForResult 发送的 Intent(此处会匹配所有导出的且 intent-filter 相同的组件,并由用户选择,由于SecondActivity为非导出组件,默认将会打开三方应用的Activity)。
再通过三方应用执行 setResult 函数,就会绕过 SecondActivity 原有的代码执行逻辑
利用点二:造成任意文件读写
另外一种情况为,应用能够接收到外部通过 startActivityForResult 传入的 Intent,由于处理和配置不当,导致三方应用能够越权读写文件沙箱内文件。下面我们先看一个例子:
FirstActivity为导出组件,且在创建时会执行 handlingIncomingIntent 方法,该方法会接收 Intent 并通过 setResult 返回结果,此处接收的 Intent 和返回结果的 Intent 为同一个Intent。攻击者可以利用这一点,在发送的 Intent 中添加 flag
FLAG_GRANT_URI_READ_PERMISSION 和/或 FLAG_GRANT_URI_WRITE_PERMISSION,那么通过 setResult 返回的 Intent 就拥有了读写本应用文件沙箱的权限。
由file_paths.xml中规定的范围,我们能够确定该FileProvider能够读写应用私有目
录下的所有文件,那么可以构造content://com.example.victim.fileprovider/root_file/data/data/com.example.victim/test.txt完成应用沙箱内文件的读写。
运行之后,能在日志中看到执行成功,且进入应用私有目录也可以看到该文件:
下面让我们看一个真实的CVE案例来加深一下印象——CVE-2021-41256 NextCloud News 提权漏洞。
NextCloud News APP 0.9.9.62 版本中存在一个导出的SettingsActivity,查看它在AndroidManifest中的定义,能够发现它拥有 intent-filter ,可以通过发送隐式 intent 拉起。
这样,就满足了Intent重定向的条件——任意应用都可以使用 startActivityForResult 方法拉起 SettingsActivity ,并且可以返回需要的 Intent。
那么,攻击者可以利用这一点,在发送的 Intent 中添加 flag FLAG_GRANT_URI_READ_PERMISSION 和/或 FLAG_GRANT_URI_WRITE_PERMISSION,
如上文所述,这样就能够读写含有属性android:grantUriPermissions="true"的Content Provider。
进一步可以发现,AndroidManifest中声明了一个FileProvider。
通过以上的信息,攻击者就可以创建一个授予URI权限 FLAG_GRANT_URI_READ_PERMISSION和FLAG_GRANT_URI_WRITE_PERMISSION,并设置data URI为content://de.luhmer.owncloudnewsreader.provider/external_files/test.txt 的 Intent 使用startActivityForResult方法拉起SettingsActivity就能够实现越权文件读写了,详细的POC代码如下:
关于这一类问题,由于其危害性比较严重,结合Google官方给出的修复建议,总结了以下几条修复建议供参考。
方案一:权限最小化
如果受影响的应用组件不需要接收来自其他应用的 Intent,可以将此应用组件设为专用组件,即在AndroidManifest中设置 android:exported="false" 。
同时,如果 Content Provider 不需要任意数据授予权限,则可以在
AndroidManifest中设置android:grantUriPermissions="false"。
除此以外,在使用 FileProvider 时,对于FILE_PROVIDER_PATHS的配置,参考源码,尽量使用除root-path以外的TAG以减小风险。
若将上文中file_paths.xml文件内容改为图中所示的内容,那么攻击者只能读写 /data/data/com.example.victim/files/test 目录下文件,有效控制了问题的影响范围
方案二:确保提取的 Intent 来自可信的来源且发送的 Intent 目标为可信方
Google在修复建议中使用 getCallingActivity 等方法来验证源 Activity 是否可信。例如:
但是需要注意的是,检查getCallingActivity() 是否返回非null值或检查包名是否匹配仅在startActivityForResult中生效,且并不足以防范此漏洞,恶意应用可以为该函数提供null值,或者伪造满足条件的包名绕过校验。故此处建议在setResult或onActivityResult时对应用的身份同时进行校验,保证收到的intent来源于可信方。
同时,应避免startActivityFor使用隐式 Intent 的方式拉起目标Activity,这样可能存在三方劫持,进而绕过校验的风险,使用显式 Intent 方式样例如下:
方案三:确保要重定向的 Intent 安全无害
对于需要重定向的 Intent ,应该进行验证,保证该 Intent 不会无校验发送到私有的组件内,并且不会被发送到外部应用的组件。如果业务需要发送到外部应用,也要确保 Intent 没有访问任意Content Provider的能力。
应用可以使用 getFlags 等方法来检查 Intent 是否会授予 URI 权限。例如:
应用还可以使用 removeFlags 撤消 URI 权限的授予。例如:
除了上述两种方法,还可以在setResult前重新创建 Intent,确保不安全的 Intent 不会被传递。
参考文献
[1] Activity 参考:https://developer.android.com/reference/android/app/Activity
[2] Intent 参考:https://developer.android.com/reference/android/content/Intent
[3] GHSL-2021-1033: Intent URI permission manipulation in Nextcloud News for Android - CVE-2021-41256: https://securitylab.github.com/advisories/GHSL-2021-1033_Nextcloud_News_for_Android/
[4] NextCloud News App: https://github.com/nextcloud/news-android
[5] startActivityForResult的简单使用总结: https://www.jianshu.com/p/acaa50c35811
[6] FileProvider 参考:https://developer.android.com/reference/androidx/core/content/FileProvider
[7] Android FileProvider配置使用:
https://www.jianshu.com/p/e9043ab9dc69
vivo刘洪善:做安全行业的长期主义者,vivo全面守护用户隐私