Android 11 新增AppOps隐私访问审查原理分析

2,330 阅读7分钟

背景

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照明弹);