作者:字節跳動無恒實驗室
原文鏈接:https://mp.weixin.qq.com/s/CY2nLUb2VQaBNxAKd7GeUQ

本文為404星鏈計劃項目 Appshark 實戰操作,分享使用 Appshark 挖掘到 2 個 CVE 漏洞的案例。

項目地址:http://github.com/bytedance/appshark
404星鏈計劃:https://github.com/knownsec/404StarLink

一、背景

LaunchAnywhere是安卓最為經典的漏洞類型之一,現在被Google稱為Intent Redirection:https://support.google.com/faqs/answer/9267555?hl=en。無恒實驗室一直對該類型漏洞有研究,我們把這一類問題比作“安卓上的SSRF”,其中Intent就像一個HTTP請求,而未經驗證完全轉發了這個請求在安卓上會導致嚴重的安全問題。關于這類漏洞的邏輯與利用,推薦閱讀:http://retme.net/index.php/2014/08/20/launchAnyWhere.html這篇文章。

本文將介紹使用appshark引擎挖掘AOSP中Intent Redirection漏洞的一個實際例子,發現的問題被Google評為高危并授予了CVE-2021-39707 & CVE-2022-20223。appshark為無恒實驗室自研的自動化漏洞及隱私合規檢測工具,當前工具已開源,歡迎感興趣的朋友試用,開源地址:http://github.com/bytedance/appshark

二、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中的位置為

https://android.googlesource.com/platform/packages/apps/Settings/+/refs/tags/android-12.0.0_r30/src/com/android/settings/users/AppRestrictionsFragment.java#630

        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旁邊的齒輪):

當我們點擊這個設置選項時,一個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是兩個互不相關的變量

(https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/content/Intent.java#7276):

    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,在注意到需要進行安全檢查的前提下,仍然在一個十幾行的函數中寫出了兩個高危漏洞。

五、關于無恒實驗室

無恒實驗室是由字節跳動資深安全研究人員組成的專業攻防研究實驗室,致力于為字節跳動旗下產品與業務保駕護航。通過漏洞挖掘、實戰演練、黑產打擊、應急響應等手段,不斷提升公司基礎安全、業務安全水位,極力降低安全事件對業務和公司的影響程度。無恒實驗室希望持續與業界共享研究成果,協助企業避免遭受安全風險,亦望能與業內同行共同合作,為網絡安全行業的發展做出貢獻。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2002/