一、布景

LaunchAnywhere是安卓最为经典的缝隙类型之一,现在被Google称为Intent Redirection:support.google.com/faqs/answer… 无恒实验室一直对该类型缝隙有研讨,咱们把这一类问题比作“安卓上的SSRF”,其间Intent就像一个HTTP恳求,而未经验证完全转发了这个恳求在安卓上会导致严峻的安全问题。关于这类缝隙的逻辑与运用,引荐阅览retme.net/index.php/2… 这篇文章。

本文将介绍运用appshark引擎发掘AOSP中Intent Redirection缝隙的一个实际比如,发现的问题被Google评为高危并颁发了CVE-2021-39707 & CVE-2022-20223。appshark为无恒实验室自研的自动化缝隙及隐私合规检测东西,当时东西已开源,欢迎感兴趣的朋友试用,开源地址:github.com/bytedance/a…

二、appshark规矩编写

为了简化问题,咱们运用一个十分根底的规矩IntentRedirectionBabyVersion

{
    "IntentRedirectionNoSan": {
      "enable": true,
      "SliceMode": true,
      "traceDepth": 6,
      "desc": {
        "name": "IntentRedirectionBabyVersion",
        "category": "IntentRedirection",
        "detail": "Intent redirection, but a very basic version",
        "wiki": "",
        "possibility": "2",
        "model": "high"
      },
      "entry": {},
      "source": {
        "Return": [
          "<android.content.Intent: android.os.Parcelable getParcelable*(java.lang.String)>",
          "<android.os.Bundle: android.os.Parcelable getParcelable*(java.lang.String)>"
        ]
      },
      "sink": {
        "<*: * startActivit*(*)>": {
          "LibraryOnly": true,
          "TaintParamType": [
            "android.content.Intent",
            "android.content.Intent[]"
          ],
          "TaintCheck": [
            "p*"
          ]
        }
      }
    }
  }

能够看到这个规矩只是考虑从getParcelable到startActivity的数据流,且不考虑sanitizer。这和咱们实际运用的规矩有一些不同,但足够阐明问题。

这儿咱们扫描的目标是com.android.settings,也便是“Settings”运用。作为一个具有system uid的高权限运用,Settings是AOSP缝隙发掘的常见目标。

三、人工排查与缝隙原理

3.1 缝隙原理

扫描出的成果较多,并不是全都可用的,尤其是咱们并没有设置任何的sanitizer * *经过人工逐个查看,咱们发现这一条扫描成果看上去可运用性很高:

{
    "details": {
        "position": "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>",
        "Sink": [
            "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>->$r2_1"
        ],
        "entryMethod": "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>",
        "Source": [
            "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>->$r5"
        ],
        "url": "/Users/admin/submodules/appshark/out/vulnerability/17-IntentRedirectionBabyVersion.html",
        "target": [
            "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>->$r5",
            "<com.android.settings.users.AppRestrictionsFragment$RestrictionsResultReceiver: void onReceive(android.content.Context,android.content.Intent)>->$r2_1"
        ]
    },
    "hash": "9bfcf0665601df186b025859e4f4c2df4e5f9cb2",
    "possibility": "2"
}

其对应的代码在AOSP中的方位为android.googlesource.com/platform/pa…

        public void onReceive(Context context, Intent intent) {
            Bundle results = getResultExtras(true);
            final ArrayList<RestrictionEntry> restrictions = results.getParcelableArrayList(
                    Intent.EXTRA_RESTRICTIONS_LIST);
            Intent restrictionsIntent = results.getParcelable(CUSTOM_RESTRICTIONS_INTENT);
            if (restrictions != null && restrictionsIntent == null) {
                onRestrictionsReceived(preference, restrictions);
                if (mRestrictedProfile) {
                    mUserManager.setApplicationRestrictions(packageName,
                            RestrictionsManager.convertRestrictionsToBundle(restrictions), mUser);
                }
            } else if (restrictionsIntent != null) {
                preference.setRestrictions(restrictions);
                if (invokeIfCustom && AppRestrictionsFragment.this.isResumed()) {
                    assertSafeToStartCustomActivity(restrictionsIntent);
                    int requestCode = generateCustomActivityRequestCode(
                            RestrictionsResultReceiver.this.preference);
                    AppRestrictionsFragment.this.startActivityForResult(
                            restrictionsIntent, requestCode);
                }
            }
        }

留意到Google考虑了这个当地有可能存在Intent Redirection导致的越权,因而增加了一个assertSafeToStartCustomActivity作为安全查看:

        private void assertSafeToStartCustomActivity(Intent intent) {
            // Activity can be started if it belongs to the same app
            if (intent.getPackage() != null && intent.getPackage().equals(packageName)) {
                return;
            }
            // Activity can be started if intent resolves to multiple activities
            List<ResolveInfo> resolveInfos = AppRestrictionsFragment.this.mPackageManager
                    .queryIntentActivities(intent, 0 /* no flags */);
            if (resolveInfos.size() != 1) {
                return;
            }
            // Prevent potential privilege escalation
            ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
            if (!packageName.equals(activityInfo.packageName)) {
                throw new SecurityException("Application " + packageName
                        + " is not allowed to start activity " + intent);
            }
        }
    }

然而,这个十几行的查看函数远远不够安全,现在咱们知道其间实际上躲藏了两个能够被绕过的逻辑。最开始被留意到的是第7-11行的代码,假设有多个Activity契合这个Intent,则这个查看会直接经过:

            // Activity can be started if intent resolves to multiple activities
            List<ResolveInfo> resolveInfos = AppRestrictionsFragment.this.mPackageManager
                    .queryIntentActivities(intent, 0 /* no flags */);
            if (resolveInfos.size() != 1) {
                return;
            }

Intent假设有多个契合的Activity,会触发用户挑选的逻辑。即便咱们假定这个挑选过程中用户不会因为操作发生安全问题,只是依托resolveInfos.size() != 1 也不能确保挑选流程会出现,原因是Activity在Manifest中有一个配置叫做android:priority ,即优先级。这个配置在AOSP的体系运用中很常见,当Intent能够resolve到多个Activity时,假设其间存在高优先级的Activity则会被直接挑选,并不会触发用户挑选的流程。因而,假设咱们能找到某个存在priority > 0 且本身具有运用价值的Activity,则能够直接经过Intent Redirection进行运用。很不巧的是,最常见的可运用Activity正好满意这一条件:

        <activity-alias android:name="PrivilegedCallActivity"
             android:targetActivity=".components.UserCallActivity"
             android:permission="android.permission.CALL_PRIVILEGED"
             android:exported="true"
             android:process=":ui">
            <intent-filter android:priority="1000">
                <action android:name="android.intent.action.CALL_PRIVILEGED"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:scheme="tel"/>
            </intent-filter>

PrivilegedCallActivity需求CALL_PRIVILEGED权限才干被调用,这一权限只是赋予体系运用,第三方运用无法获得。经过这个Activity咱们能够直接让手机拨打恣意电话(包括紧急电话),适宜的运用能够形成“窃听”的效果。

3.2 威胁场景

要触发这个缝隙,咱们需求先了解AppRestrictionsFragment是用来做什么的。实际上,安卓供给一种叫做“Restricted Profile”的受限用户类型,通常在安卓平板上运用。这类用户能够运用的APP以及能看到的内容都能够被主用户操控。在安卓手机上,咱们能够经过adb指令增加这类用户:

adb shell pm create-user --restricted restricted-user

之后在多用户的设置界面就能够看到受限用户,而AppRestrictionsFragment便是用来操控该用户能运用哪些APP的。除了设置APP启用与否,还能对APP进行独自的设置(留意PwnRestricted旁边的齿轮):

AOSP Bug Hunting with appshark (1): Intent Redirection

当咱们点击这个设置选项时,一个action为android.intent.action.GET_RESTRICTION_ENTRIES 的Intent会发送给对应APP,因而咱们的PoC中需求定义一个满意条件的Receiver来接收Intent。在这个Receiver里,咱们需求把恶意Intent放在result的EXTRA_RESTRICTIONS_INTENT中。一起,为了满意前文提到的“多个Activity契合Intent”的条件,咱们还需求自定义一个Activity,它的filter和PrivilegedCallActivity相同:

            <intent-filter>
                <action android:name="android.intent.action.CALL_PRIVILEGED" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="tel" />
            </intent-filter>

这个Activity并不会被start,原因是PrivilegedCallActivity的优先级更高。

至此,点击AppRestrictionsFragment界面的PoC运用设置图标,就会直接发动PrivilegedCallActivity拨打电话,整个运用就完成了。这便是CVE-2021-39707,一个完全可控的Intent Redirection,但需求用户交互才干触发。留意它现已被修复了,因而在最新版别的安卓上无法复现。

3.3 One More Bug

当上文的缝隙被修复之后,我在回顾时发现,assertSafeToStartCustomActivity还存在另一个问题,就在第一个if:

            // Activity can be started if it belongs to the same app
            if (intent.getPackage() != null && intent.getPackage().equals(packageName)) {
                return;
            }

这一段的逻辑是,假设intent的package和PoC相同,阐明是翻开PoC自己的Activity,那就能够经过查看。简略看上去同样没问题,然而Intent有个十分特殊的当地,即Component和Package是两个互不相关的变量 (android.googlesource.com/platform/fr…

    private String mPackage;
    private ComponentName mComponent;

而在resolve一个Intent时,Component的优先级是最高的,当它被设置时,mPackage会被直接疏忽。因而,假设咱们有一个一起设置了Package和Component的Intent,就能够直接满意assertSafeToStartCustomActivity的查看,乃至不需求一个高优先级的Activity。这样咱们发现了第二个高危缝隙,也便是CVE-2022-20223。

四、总结

经过这篇文章咱们看到,即便仅有一条十分简略的缝隙规矩,appshark也能帮助咱们发现AOSP的高危缝隙。当然,扫描器不是万能的,后续的绕过及运用都需求人工剖析;但假设没有appshark,咱们从一开始就不会留意到这个当地。

在扫描规矩上,咱们只是考虑了从getParcelable到startActivity的数据流,实际上Intent Redirection的sink能够是其他组件,例如startService或是sendBroadcast,而source也未必是getParcelable。这些更多的可能就留给读者尝试,期望你也能借助appshark发现安卓运用的安全问题,或是取得自己的安卓CVE。

最终,直接运用外部的Intent来startActivity(或是发动其他类型的组件如Service)是十分危险的,开发者应当尽量防止这类行为。即便是Google,在留意到需求进行安全查看的前提下,仍然在一个十几行的函数中写出了两个高危缝隙。

五、关于无恒实验室

无恒实验室是由字节跳动资深安全研讨人员组成的专业攻防研讨实验室,致力于为字节跳动旗下产品与业务保驾护航。经过缝隙发掘、实战演练、黑产打击、应急呼应等手段,不断提高公司根底安全、业务安全水位,极力下降安全事件对业务和公司的影响程度。无恒实验室期望继续与业界继续共享研讨成果,帮忙企业防止遭受安全危险,亦望能与业内同行共同协作,为网络安全行业的开展做出贡献。