背景
android 11版本(api level 30)上新增了隐私审查api。应用使用相应的api来检查自己的应用中是否有超出意料外的隐私访问(比如接入的第三方sdk有未告知的隐私访问行为)。
api说明详见谷歌官方文档:
developer.android.google.cn/guide/topic…
从文档中我们可以得知,相应的功能实现是在AppOpsManager实现的。在这篇文档中我们将探讨AppOpsManager在这块代码上的更新。
以下代码分析基于Android 10和11版本源码。对于android 11,个人在本地使用的是lineageOS 18.1的源码,可能和aosp略有出入。
主要结构
AppOps在android 4.3版本就已经引入,对于应用开发者来说一直不可见。Ops,为Opereations的简写,意为“应用的操作、行为”。在android 6.0引入运行时权限之前,AppOps机制一直作为隐藏权限管理机制存在,国内各大手机厂商定制rom基本都会基于AppOps进行二次开发,以实现自己的权限管理。AppOps中定义的“行为”是“权限”的超集,所以这篇文章我将多用“行为管理”而不是“权限管理”来描述AppOps的操作。
AppOpsService实现了主要的行为校验机制。和其他所有的类似服务一样,在开机时启动并注入到系统服务集中。AppOpsManager主要实现了对外的接口,可供开发者及其他系统服务调用。
流程分析
我们以比较简单的Clipboard(剪贴板)服务调用来作为示例进行分析。
应用获取到ClipboardManager之后,通过ClipboardManager.getPrimaryClip接口获取粘帖板中信息。ClipboardManager通过绑定的信息获取调用者的packageName和uid,然后调用ClipboardService.getPrimaryClip方法。
在ClipboardService.getPrimaryClip方法中,可以看到调用了clipboardAccessAllowed的方法。这个方法就是验证权限和行为的方法。在这里我把关键代码展示一下:
private boolean clipboardAccessAllowed(int op, String callingPackage, int uid,
@UserIdInt int userId, boolean shouldNoteOp) {
boolean allowed = false;
// First, verify package ownership to ensure use below is safe.
mAppOps.checkPackage(uid, callingPackage);
// Shell can access the clipboard for testing purposes.
if (mPm.checkPermission(android.Manifest.permission.READ_CLIPBOARD_IN_BACKGROUND,
callingPackage) == PackageManager.PERMISSION_GRANTED) {
allowed = true;
}
// The default IME is always allowed to access the clipboard.
String defaultIme = Settings.Secure.getStringForUser(getContext().getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD, userId);
if (!TextUtils.isEmpty(defaultIme)) {
final String imePkg = ComponentName.unflattenFromString(defaultIme).getPackageName();
if (imePkg.equals(callingPackage)) {
allowed = true;
}
}
switch (op) {
case AppOpsManager.OP_READ_CLIPBOARD:
// Clipboard can only be read by applications with focus..
// or the application have the INTERNAL_SYSTEM_WINDOW and INTERACT_ACROSS_USERS_FULL
// at the same time. e.x. SystemUI. It needs to check the window focus of
// Binder.getCallingUid(). Without checking, the user X can't copy any thing from
// INTERNAL_SYSTEM_WINDOW to the other applications.
if (!allowed) {
allowed = mWm.isUidFocused(uid)
|| isInternalSysWindowAppWithWindowFocus(callingPackage);
}
if (!allowed && mContentCaptureInternal != null) {
// ...or the Content Capture Service
// The uid parameter of mContentCaptureInternal.isContentCaptureServiceForUser
// is used to check if the uid has the permission BIND_CONTENT_CAPTURE_SERVICE.
// if the application has the permission, let it to access user's clipboard.
// To passed synthesized uid user#10_app#systemui may not tell the real uid.
// userId must pass intending userId. i.e. user#10.
allowed = mContentCaptureInternal.isContentCaptureServiceForUser(uid, userId);
}
if (!allowed && mAutofillInternal != null) {
// ...or the Augmented Autofill Service
// The uid parameter of mAutofillInternal.isAugmentedAutofillServiceForUser
// is used to check if the uid has the permission BIND_AUTOFILL_SERVICE.
// if the application has the permission, let it to access user's clipboard.
// To passed synthesized uid user#10_app#systemui may not tell the real uid.
// userId must pass intending userId. i.e. user#10.
allowed = mAutofillInternal.isAugmentedAutofillServiceForUser(uid, userId);
}
break;
case AppOpsManager.OP_WRITE_CLIPBOARD:
// Writing is allowed without focus.
allowed = true;
break;
default:
throw new IllegalArgumentException("Unknown clipboard appop " + op);
}
if (!allowed) {
Slog.e(TAG, "Denying clipboard access to " + callingPackage
+ ", application is not in focus nor is it a system service for "
+ "user " + userId);
return false;
}
// Finally, check the app op.
int appOpsResult;
if (shouldNoteOp) {
appOpsResult = mAppOps.noteOp(op, uid, callingPackage);
} else {
appOpsResult = mAppOps.checkOp(op, uid, callingPackage);
}
return appOpsResult == AppOpsManager.MODE_ALLOWED;
}
首先我们看到调用了mAppOps.checkPackage(uid, callingPackage);,这里主要是为了保证调用者的包名和uid是属于同一个应用的。个人理解主要是为了规避跨应用的攻击行为,增强安全性。
然后是调用了PackageManager的鉴权方法。mPm.checkPermission,校验通过会设置标志位为true。
另外默认的IME(输入法)可以设置始终允许应用获取剪切板信息。这里通过检查了同样会设置标志位为true。
然后就是校验op(operation,行为)。这里依旧是检查是否有操作的权限,比如普通应用如果未获取焦点,是不允许读取剪切板的。
通过了上述所有检查之后,在最后,会调用AppOpsManager来进行行为检查。
noteOp和checkOp的主要表现是一样的,最大的不同点是noteOp会在校验行为的同时,对行为进行记录。这个方法的返回值介绍如下:
-
MODE_ALLOWED:故名思议,允许操作。
-
MODE_IGNORED:“忽略”,应用未拥有相应操作的权限,但是此时会静默拒绝,不会抛出异常导致应用crash。
-
MODE_ERRORED:和MODE_IGNORED类似,同样是应用不具有相应权限。但是此时framework会抛出一个SecurityException异常。
-
MODE_DEFAULT:不常见
-
MODE_FOREGROUND:故名意思,只有应用在前台的时候才被允许相应操作。
我们从noteOp这个方法继续往下的分析。可以发现在最后会调用到AppOpsManager.noteOpNoThrow方法中。这个方法在android 10上的实现极为简单,但是在android 11上做了很大的改变。我们对比一下前后的变化:
android 10
public int noteOpNoThrow(int op, int uid, String packageName) {
try {
return mService.noteOperation(op, uid, packageName);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
android 11
public int noteOpNoThrow(int op, int uid, @Nullable String packageName,
@Nullable String attributionTag, @Nullable String message) {
try {
collectNoteOpCallsForValidation(op);
int collectionMode = getNotedOpCollectionMode(uid, packageName, op);
boolean shouldCollectMessage = Process.myUid() == Process.SYSTEM_UID ? true : false;
if (collectionMode == COLLECT_ASYNC) {
if (message == null) {
// Set stack trace as default message
message = getFormattedStackTrace();
shouldCollectMessage = true;
}
}
int mode = mService.noteOperation(op, uid, packageName, attributionTag,
collectionMode == COLLECT_ASYNC, message, shouldCollectMessage);
if (mode == MODE_ALLOWED) {
if (collectionMode == COLLECT_SELF) {
collectNotedOpForSelf(op, attributionTag);
} else if (collectionMode == COLLECT_SYNC) {
collectNotedOpSync(op, attributionTag);
}
}
return mode;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
可以看到android 11上主要是增加了收集调用者信息的相关方法。
首先通过collectNoteOpCallsForValidation进行信息收集。这个方法内部如下:
private void collectNoteOpCallsForValidation(int op) {
if (NOTE_OP_COLLECTION_ENABLED) {
try {
mService.collectNoteOpCallsForValidation(getFormattedStackTrace(),
op, mContext.getOpPackageName(), mContext.getApplicationInfo().longVersionCode);
} catch (RemoteException e) {
// Swallow error, only meant for logging ops, should not affect flow of the code
}
}
}
通过getFormattedStackTrace获取现在的调用栈信息,然后将信息传入AppOpsService中,以供存入文件。因为AppOpsService属于跨进程调用,无法直接获取调用者的堆栈信息,而AppOpsManager可以做到,所以才将这部分逻辑代码放置在AppOpsManager中实现。
我们继续跟随noteOpNoThrow中的代码。调用了getNotedOpCollectionMode方法来判断当前是否要进行信息收集。这部分判断会决定最后是否调用collectNotedOpForSelf或者collectNotedOpSync。从源码中我们可以得知,这两块的信息收集是传递给OnOpNotedCallback的,用户通过AppOpsManager设置的隐私代码审计就是通过这个接口。
getNotedOpCollectionMode方法会从sAppOpsToNote数组中取得判断结果,这个数组是一个简单的缓存结构,在没有缓存的时候会从AppOpsService.shouldCollectNotes中取得结果。这个方法的代码如下:
public boolean shouldCollectNotes(int opCode) {
Preconditions.checkArgumentInRange(opCode, 0, _NUM_OP - 1, "opCode");
String perm = AppOpsManager.opToPermission(opCode);
if (perm == null) {
return false;
}
PermissionInfo permInfo;
try {
permInfo = mContext.getPackageManager().getPermissionInfo(perm, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return permInfo.getProtection() == PROTECTION_DANGEROUS
|| (permInfo.getProtectionFlags() & PROTECTION_FLAG_APPOP) != 0;
}
最关键的判断其实是最后一行。前一句permInfo.getProtection() == PROTECTION_DANGEROUS比较好理解,当权限是PROTECTION_DANGEROUS级别的时候,返回true。后一句涉及到一个额外的标志,appop。这里我用举例的方式解释,我们定位到framework/base/core/res/AndroidManifest.xml,查看ACCESS_NOTIFICATIONS这个权限:
<permission android:name="android.permission.ACCESS_NOTIFICATIONS"
android:protectionLevel="signature|privileged|appop" />
像这种在protectionLevel标签中附加appop的权限,哪怕不是PROTECTION_DANGEROUS级别,同样会返回为true。但是我们可以看到,附加了这种标签的权限非常少,只有寥寥几个。所以大部分非敏感权限,都是不会触发OnOpNotedCallback的回调的。但是不排除google将来收紧的可能。
总结
可以看到,android中的“权限校验”不止permissionManager,还混杂了AppOps在其中。两者共同完成了权限校验。但是AppOps的检查更为宽泛,很多没有被定义为“权限”的行为,也会由AppOps来进行检查。
AppOps在android 11上有了很大更新,添加了隐私审查机制,记录的信息也更加全面。对于应用开发者来说,可以通过新api来审查自己的应用是否有意料之外的隐私访问,规避法律风险。对于framework开发者来说,可以通过新的api来更轻松地实现类似隐私访问统计的功能(如MIUI照明弹);