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:
這個漏洞的Android ID是17356824,影響4.0.1到4.4.4之間的版本,時間跨度從2011年到2014年,應該說影響面非常廣,根據今年11月Google的統計,這個區間的Android設備在全球的占比大約為90%。
retme給該漏洞起了一個很給力的名字broadcastAnywhere,與launchAnywhere相比,這兩個漏洞的相同點在于:
都是利用了addAccount這個機制,一個惡意app通過注冊為account的authenticator并處理某賬號類型,然后發送intent給settings app,讓其添加該特定類型的賬號。
都是利用settings這個應用具有SYSTEM權限,誘使settings來發送一個高權限的intent。
不同點在于:
本質原理不同:一個是惡意app返回一個intent被settings launch,另外一個是settings 發出一個pendingintent給惡意app,而惡意app利用pendingintent的特點來修改pendingitent的action與extras,并以settings的身份發出。
漏洞代碼位置不同:一個是accountmanger中,一個是settings中
后果不同:launchAnywhere是以system權限啟動activity,而broadcastAnywhere是一個system權限發送 broadcast。前者往往需要界面,而后者不需要界面。
本文是對retme分析的一個補充,同時也給大家分享一下在挖掘這個漏洞中的一些經驗,當然為了完整性,我也盡量系統地描述相關的內容。由于時間倉促,難免有遺漏與不當之處,請各位不吝指正。
關于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的安全風險主要發生在下面兩個條件同時滿足的場景下:
原因是,如果原始Intent的Component與action都為空(“雙無”Intent),B就可以通過修改action來將Intent發送向那些聲明了intent filter的組件,如果A是一個有高權限的APP(如settings就具有SYSTEM權限),B就可以以A的身份做很多事情。
當然上面描述的是大多數情況。一些極端的情況下,比如某些情況下B雖然無法修改action將Intent發送到其他組件,但依然可以放入額外的數據,如果該組件本身接收數據時未考慮周全,也是存在風險的。
如果你閱讀過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接口:
在Android 5.0的源碼中,修復方法是設置了一個虛構的Action與Component https://android.googlesource.com/platform/packages/apps/Settings/+/37b58a4%5E%21/#F0
最初報告這個漏洞給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外,系統還提供“小米賬號”供選擇,由于不是單選,系統會彈出一個對話框供用戶選擇:
當然如果用戶已經添加了小米賬號,就只剩下一個選項,攻擊就無需人工干預了。這部分的具體流程可以參考Android源碼以及MIUI代碼中Settings應用的ChooseAccountActivity.java部分,這里不再贅述。
另外,按照google官方文檔,一個app要注冊成為賬號的authenticator,需要一個權限:android.permission.AUTHENTICATE_ACCOUNTS。 retme博客中的POC也申請了這些權限。但實際測試中發現,這個權限可以去掉。所以這個漏洞等同于一個無任何權限APP的提權漏洞。
前面提到,這種漏洞大多數情況下,僅對“雙無”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
這里,盡管普通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
這里,由于該PendingIntent通過一個非顯式的Intent發送,惡意APP可以劫持這個Intent,從而導致權限泄露。
另外一種動態分析的方法是通過dumpsys來觀察當前系統中的PendingIntent Record,例如5.0修復后,觀察到的Settings發送的PendingIntent有了act與cmp屬性,而5.0之前的為空。
【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