安卓Bug 17356824 BroadcastAnywhere漏洞分析

wyzsk發表於2020-08-19
作者: neobyte · 2014/11/16 15:20

0x00 背景


2014年8月,retme分析了Android修復的一個漏洞,並命名為launchAnyWhere1

在除錯這個漏洞的時候,我發現Settings應用還存在一個類似漏洞,並在9月報告給了Android Security Team,標題為,Privilege escalation vulnerability in settings app of android 4.0 to 4.4 (leads to phishing sms), 並且很快得到了確認,Android官方也給了致謝2

enter image description here

這個漏洞的Android ID是17356824,影響4.0.1到4.4.4之間的版本,時間跨度從2011年到2014年,應該說影響面非常廣,根據今年11月Google的統計,這個區間的Android裝置在全球的佔比大約為90%。

a

retme給該漏洞起了一個很給力的名字broadcastAnywhere,與launchAnywhere相比,這兩個漏洞的相同點在於:

  1. 都是利用了addAccount這個機制,一個惡意app透過註冊為account的authenticator並處理某賬號型別,然後傳送intent給settings app,讓其新增該特定型別的賬號。

  2. 都是利用settings這個應用具有SYSTEM許可權,誘使settings來傳送一個高許可權的intent。

不同點在於:

  1. 本質原理不同:一個是惡意app返回一個intent被settings launch,另外一個是settings 發出一個pendingintent給惡意app,而惡意app利用pendingintent的特點來修改pendingitent的action與extras,並以settings的身份發出。

  2. 漏洞程式碼位置不同:一個是accountmanger中,一個是settings中

  3. 後果不同:launchAnywhere是以system許可權啟動activity,而broadcastAnywhere是一個system許可權傳送 broadcast。前者往往需要介面,而後者不需要介面。

本文是對retme分析的一個補充,同時也給大家分享一下在挖掘這個漏洞中的一些經驗,當然為了完整性,我也儘量系統地描述相關的內容。由於時間倉促,難免有遺漏與不當之處,請各位不吝指正。

0x01 PendingIntent的風險


關於PendingIntent,簡單理解是一種非同步傳送的intent,通常被使用在通知Notification的回撥,短訊息SmsManager的回撥和警報器AlarmManager的執行等等,是一種使用非常廣的機制。對PendingIntent的深入分析,可以參考該文【4】:

但是關於PendingIntent的安全意義,討論不多,在官方的開發文件中,特別註明:【5】:

By giving a PendingIntent to another application, you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). As such, you should be careful about how you build the PendingIntent: almost always, for example, the base Intent you supply should have the component name explicitly set to one of your own components, to ensure it is ultimately sent there and nowhere else.

從上面的英文來看,大意是當A設定一個原始Intent(base intent)並據此建立PendingIntent,並將其交給B時,B就可以以A的身份來執行A預設的操作(傳送該原始Intent),並擁有A同樣的許可權與ID。因此,A應當小心設定這個原始Intent,務必具備顯式的Component,防止許可權洩露。

許可權洩露的風險在於,B得到這個PendingIntent後,還可以對其原始Intent進行有限的修改,這樣就可能以A的許可權與ID來執行A未預料的操作。

但實際上,這裡的限制很多,甚至有點雞肋。因為本質上這個修改是透過Intent.fillIn來實現的,因此可以檢視fillin的原始碼:

如下面原始碼所示,B可以修改的資料可以分成兩類:

1 action,category,data,clipdata,package這些都可以修改,只要原來為空,或者開發者設定了對應的標誌。

2 但selector與component,不論原來是否為空,都必須由開發者設定顯式的標誌才能修改

#!java
public int fillIn(Intent other, int flags) {
    int changes = 0;
    if (other.mAction != null
            && (mAction == null || (flags&FILL_IN_ACTION) != 0)) {//當本action為空或者開發者設定了FILL_IN_ACTION標誌時,可以修改action
        mAction = other.mAction;
        changes |= FILL_IN_ACTION;
    }
    if ((other.mData != null || other.mType != null)
            && ((mData == null && mType == null)
                    || (flags&FILL_IN_DATA) != 0)) {//類似action,需要data與type同時為空
        mData = other.mData;
        mType = other.mType;
        changes |= FILL_IN_DATA;
    }
    if (other.mCategories != null
            && (mCategories == null || (flags&FILL_IN_CATEGORIES) != 0)) {//類似action
        if (other.mCategories != null) {
            mCategories = new ArraySet<String>(other.mCategories);
        }
        changes |= FILL_IN_CATEGORIES;
    }
    if (other.mPackage != null
            && (mPackage == null || (flags&FILL_IN_PACKAGE) != 0)) {//類似action
        // Only do this if mSelector is not set.
        if (mSelector == null) {
            mPackage = other.mPackage;
            changes |= FILL_IN_PACKAGE;
        }
    }
    // Selector is special: it can only be set if explicitly allowed,
    // for the same reason as the component name.
    if (other.mSelector != null && (flags&FILL_IN_SELECTOR) != 0) {//必須設定了FILL_IN_SELECTOR才可以修改selector
        if (mPackage == null) {//selector與package是互斥的
            mSelector = new Intent(other.mSelector);
            mPackage = null;
            changes |= FILL_IN_SELECTOR;
        }
    }
    if (other.mClipData != null
            && (mClipData == null || (flags&FILL_IN_CLIP_DATA) != 0)) {//類似action
        mClipData = other.mClipData;
        changes |= FILL_IN_CLIP_DATA;
    }
    // Component is special: it can -only- be set if explicitly allowed,
    // since otherwise the sender could force the intent somewhere the
    // originator didn't intend.
    if (other.mComponent != null && (flags&FILL_IN_COMPONENT) != 0) {//必須開發者設定FILL_IN_COMPONENT才可以修改component
        mComponent = other.mComponent;
        changes |= FILL_IN_COMPONENT;
    }
    mFlags |= other.mFlags;
    if (other.mSourceBounds != null
            && (mSourceBounds == null || (flags&FILL_IN_SOURCE_BOUNDS) != 0)) {
        mSourceBounds = new Rect(other.mSourceBounds);
        changes |= FILL_IN_SOURCE_BOUNDS;
    }
    if (mExtras == null) {//Extras資料被合併
        if (other.mExtras != null) {
            mExtras = new Bundle(other.mExtras);
        }
    } else if (other.mExtras != null) {
        try {
            Bundle newb = new Bundle(other.mExtras);
            newb.putAll(mExtras);
            mExtras = newb;
        } catch (RuntimeException e) {

而一般開發者都不會去顯式設定這個標誌(教材裡沒人這麼教),所以通常情況下,B無法修改原始Intent的Component,而僅當原始Intent的action為空時,可以修改action。

所以大多數情況下,PendingIntent的安全風險主要發生在下面兩個條件同時滿足的場景下:

  1. 構造PendingIntent時的原始Intent既沒有指定Component,也沒有指定action
  2. 將PendingIntent洩露給第三方

原因是,如果原始Intent的Component與action都為空(“雙無”Intent),B就可以透過修改action來將Intent傳送向那些宣告瞭intent filter的元件,如果A是一個有高許可權的APP(如settings就具有SYSTEM許可權),B就可以以A的身份做很多事情。

當然上面描述的是大多數情況。一些極端的情況下,比如某些情況下B雖然無法修改action將Intent傳送到其他元件,但依然可以放入額外的資料,如果該元件本身接收資料時未考慮周全,也是存在風險的。

0x02 Settings中的PendingIntent漏洞


如果你閱讀過retme關於launchAnywhere的分析【1】,就會了解Settings的addAccount機制:一個惡意APP可以註冊一種獨有的賬號型別併成為該型別賬號的認證者(Authenticator),透過傳送Intent來促使Settings新增該型別賬號時,Settings將呼叫惡意APP提供的介面。而這個過程,就不幸將一個“雙無”PendingIntent發給了惡意APP。

看看安卓4.4.4的Settings中有漏洞的原始碼:可見一個mPendingIntent是透過new Intent()構造原始Intent的,所以為“雙無”Intent,這個PendingIntent最終被透過AccountManager.addAccount方法傳遞給了惡意APP介面:

enter image description here

在Android 5.0的原始碼中,修復方法是設定了一個虛構的Action與Component https://android.googlesource.com/platform/packages/apps/Settings/+/37b58a4%5E%21/#F0

enter image description here

最初報告這個漏洞給Android時,用的偽造簡訊的POC,也是retme部落格中演示的。例如可以偽造10086傳送的簡訊,這與收到正常簡訊的表象完全一致(並非有些APP申請了WRITE_SMS許可權後直接寫簡訊資料庫時無接收提示)。後來又更新了一個Factory Reset的POC,可以強制無任何提示將使用者手機恢復到出廠設定,清空簡訊與通訊錄等使用者資料,惡意APP的介面程式碼片段如下:

#!java
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
//這裡透過getParcelable(“pendingintent”)就獲得了settings傳過來的“雙無”PendingIntent:

  PendingIntent test = (PendingIntent)options.getParcelable("pendingIntent"); 

  Intent newIntent2 = new Intent("android.intent.action.MASTER_CLEAR");
  try {
            test.send(mContext, 0, newIntent2, null, null);
  } catch (CanceledException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
  }

該攻擊在一些國內的主流手機中測試成功。大多數情況下,攻擊是自動的,無需使用者干預,過程與launchAnywhere類似。

有意思的是,在小米手機中,如果使用者未新增小米賬號,那麼該攻擊需要使用者干預才能成功:原因是MIUI修改了Settings程式,當新增賬號時,對任意賬號型別,除了對應的authenticator外,系統還提供“小米賬號”供選擇,由於不是單選,系統會彈出一個對話方塊供使用者選擇:

enter image description here

當然如果使用者已經新增了小米賬號,就只剩下一個選項,攻擊就無需人工干預了。這部分的具體流程可以參考Android原始碼以及MIUI程式碼中Settings應用的ChooseAccountActivity.java部分,這裡不再贅述。

另外,按照google官方文件,一個app要註冊成為賬號的authenticator,需要一個許可權:android.permission.AUTHENTICATE_ACCOUNTS。 retme部落格中的POC也申請了這些許可權。但實際測試中發現,這個許可權可以去掉。所以這個漏洞等同於一個無任何許可權APP的提權漏洞。

0x03 類似漏洞的發現


前面提到,這種漏洞大多數情況下,僅對“雙無”Intent(無Action無Component)構造的PendingIntent有效。所以我們主要關注類似的場景。

一個發現類似漏洞的簡單策略如下:

第一步:在一個method中,如果呼叫了下面方法之一,那麼代表建立了PendingIntent,設定Priority為低:

#!java
static PendingIntent    getActivities(Context context, int requestCode, Intent[] intents, int flags)
static PendingIntent    getActivities(Context context, int requestCode, Intent[] intents, int flags, Bundle options)
static PendingIntent    getActivity(Context context, int requestCode, Intent intent, int flags)
static PendingIntent    getActivity(Context context, int requestCode, Intent intent, int flags, Bundle options)
static PendingIntent    getBroadcast(Context context, int requestCode, Intent intent, int flags)
static PendingIntent    getService(Context context, int requestCode, Intent intent, int flags)
public PendingIntent   createPendingResult(int requestCode, Intent data, int flags) 

第二步,分析該method中呼叫的方法,如果沒有呼叫下面的方法,代表未設定Component,將Priority調高到中:

#!java
Intent(Context packageContext, Class<?> cls)
Intent(String action, Uri uri, Context packageContext, Class<?> cls)
Intent  setClass(Context packageContext, Class<?> cls)
Intent  setClassName(Context packageContext, String className)
Intent  setClassName(String packageName, String className)
Intent  setComponent(ComponentName component)

第三步,再分析該method中呼叫的方法,如果沒有呼叫下面的方法,代表未設定action,很可能原始intent是“雙無”intent,那麼將Priority設定為高:

#!java
Intent(String action)
Intent(String action, Uri uri)
Intent  setAction(String action)

該策略出奇地簡單,也會有一些誤報。但實際執行該策略非常有效且不會有漏報。除了發現上面的Settings中的漏洞外,還可以發現Android原始碼(5.0版本也未修復)其他一些類似的地方,例如:

https://android.googlesource.com/platform/frameworks/opt/telephony/+/android-5.0.0_r6/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

enter image description here

這裡,儘管普通APP無法訪問其他APP的notification,但利用AccessiblyService或者 NotificationListenerService,一個APP可能可以獲取其他notification中的pendingintent,導致許可權洩露。

https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r6/keystore/java/android/security/KeyChain.java

enter image description here

這裡,由於該PendingIntent透過一個非顯式的Intent傳送,惡意APP可以劫持這個Intent,從而導致許可權洩露。

另外一種動態分析的方法是透過dumpsys來觀察當前系統中的PendingIntent Record,例如5.0修復後,觀察到的Settings傳送的PendingIntent有了act與cmp屬性,而5.0之前的為空。

enter image description here

0x04 參考資料


【1】launchAnywher:http://retme.net/index.php/2014/08/20/launchAnyWhere.html

【2】安卓官方致謝:https://source.android.com/devices/tech/security/acknowledgements.html

【3】broadcastAnywhere:http://retme.net/index.php/2014/11/14/broadAnywhere-bug-17356824.html

【4】PendingIntent的深入分析:http://my.oschina.net/youranhongcha/blog/196933

【5】官方對PendingIntent的解釋:http://developer.android.com/reference/android/app/PendingIntent.html

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章