Post

理解 App Ops

概述

根据 Google 的文档,从 Android 4.3 开始,Android 应用框架引入了 App Ops。

App Ops 涵盖了广泛的功能,被用于访问控制和跟踪,帮助运行时权限访问控制和跟踪到电池消耗。也就是说 App Ops 也被用于安全用途,具体而言:新的权限模型由传统 permission 和 App Ops 共同组成,一些权限会额外关联 op,例如 android.permission.ACCESS_FINE_LOCATION permission 就关联了 OPSTR_FINE_LOCATION 这个 App Ops,但并非所有 permission 都有对应的 op。只有 permission 额外关联的 App Ops 同时满足时,接口才允许被应用调用。

App Ops 功能的核心实现由运行在 system_server 进程中的 AppOpsService 服务提供。AppOpsService 类包含有两个主要的方法 noteOperation 和 noteProxyOperation,用于检查&记录应用的 Op。从方法名看,由于会「记录」(note),因此后面读代码会发现这两个方法都有相应的调用认证鉴权机制。

OP

op 是 operation 的缩写,定义应用的”行为/动作“,是 AppOpsService operation 访问控制的行动单位。在 AppOpsManager.java 中可以看到完整的 op 定义。

截取部分 op 常量即对应的值展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    // when adding one of these:
    //  - increment _NUM_OP
    //  - define an OPSTR_* constant (marked as @SystemApi)
    //  - add rows to sOpToSwitch, sOpToString, sOpNames, sOpToPerms, sOpDefault
    //  - add descriptive strings to Settings/res/values/arrays.xml
    //  - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)

    /** @hide No operation specified. */
    @UnsupportedAppUsage
    public static final int OP_NONE = -1;
    /** @hide Access to coarse location information. */
    @TestApi
    public static final int OP_COARSE_LOCATION = 0;
    /** @hide Access to fine location information. */
    @UnsupportedAppUsage
    public static final int OP_FINE_LOCATION = 1;
    /** @hide Causing GPS to run. */
    @UnsupportedAppUsage
    public static final int OP_GPS = 2;

通常每个 op 都对应一个 permission,这在 sOpPerms 常量中记录,并可使用 opToPermission(int op) 方法获取:

1
2
3
4
5
6
7
8
9
10
    /**
     * This optionally maps a permission to an operation.  If there
     * is no permission associated with an operation, it is null.
     */
    @UnsupportedAppUsage
    private static String[] sOpPerms = new String[] {
            android.Manifest.permission.ACCESS_COARSE_LOCATION,
            android.Manifest.permission.ACCESS_FINE_LOCATION,
            null,
            android.Manifest.permission.VIBRATE,

通过 op 的值查询对应上面的数组位置可见 ACCESS_COARSE_LOCATION,ACCESS_FINE_LOCATION 这两个 permission 对应的 op 分别是 OP_COARSE_LOCATION(0),OP_FINE_LOCATION(1) 。但要注意并非每个权限都对应有 op,反之依然。

此外,还有一个数组 sOpToSwitch 需要额外注意,它定义了每个 op 对应的 switch code,并使用 opToSwitch(int op) 转换 op 为 switch。而从后面的代码实现可以发现,实际控制 operation 的是 switch code 而非 op code,因此 switch code 可以理解为精简版的 op code。摘录部分 sOpToSwitch 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
    /**
     * This maps each operation to the operation that serves as the
     * switch to determine whether it is allowed.  Generally this is
     * a 1:1 mapping, but for some things (like location) that have
     * multiple low-level operations being tracked that should be
     * presented to the user as one switch then this can be used to
     * make them all controlled by the same single operation.
     */
    private static int[] sOpToSwitch = new int[] {
            OP_COARSE_LOCATION,                 // COARSE_LOCATION
            OP_COARSE_LOCATION,                 // FINE_LOCATION
            OP_COARSE_LOCATION,                 // GPS
            OP_VIBRATE,                         // VIBRATE

通过 op 的值查询 sOpToSwitch 数组可以发现,OP_COARSE_LOCATION,OP_FINE_LOCATION,OP_GPS 三个 op 转换为 switch code 后均为 OP_COARSE_LOCATION,即值为 0,这说明 ACCESS_COARSE_LOCATION,ACCESS_FINE_LOCATION 这两个 permission 都使用 OP_COARSE_LOCATION 这个 op 来控制。记住这一点可以用于后续解释 Android 校验 op 的行为。

延伸分析:

permissionToOpCode 还可以看到一段值得注意的注释

1
2
3
4
5
6
7
8
9
10
11
    /**
     * Retrieve the app op code for a permission, or null if there is not one.
     * This API is intended to be used for mapping runtime or appop permissions
     * to the corresponding app op.
     * @hide
     */
    @TestApi
    public static int permissionToOpCode(String permission) {
        Integer boxedOpCode = sPermToOp.get(permission);
        return boxedOpCode != null ? boxedOpCode : OP_NONE;
    }

按此注释, permission 还进一步分为 runtime permission 和 appop permission,并且在 RUNTIME_AND_APPOP_PERMISSIONS_OPS 罗列了二者分别对应的 op

1
2
3
4
5
6
7
            // APPOP PERMISSIONS
            OP_ACCESS_NOTIFICATIONS,
            OP_SYSTEM_ALERT_WINDOW,
            OP_WRITE_SETTINGS,
            OP_REQUEST_INSTALL_PACKAGES,
            OP_START_FOREGROUND,
            OP_SMS_FINANCIAL_TRANSACTIONS,

查询 sOpPerms 可以发现分别对应以下 appop permission:

1
2
3
4
5
6
android.Manifest.permission.ACCESS_NOTIFICATIONS
android.Manifest.permission.SYSTEM_ALERT_WINDOW
android.Manifest.permission.WRITE_SETTINGS
Manifest.permission.REQUEST_INSTALL_PACKAGES
Manifest.permission.FOREGROUND_SERVICE
Manifest.permission.SMS_FINANCIAL_TRANSACTIONS

其中的 android.Manifest.permission.ACCESS_NOTIFICATIONS 在 Manifest.permission 查找不到,应是 Android 系统内部使用而非面向应用开发者的权限。

有理由猜测 appop Permission 完全由 AppOpsService 实现的“权限”,区别于 AMS/PMS 实现的传统 Permission。

OP Mode

OP Mode 表示该 OP 的访问控制模式,同时也是 noteOperation,noteProxyOperation 等方法的返回值

AppOpsManager 定义了多种 Mode,包括

  • MODE_ALLOWED(0)

允许给定的调用者执行目标 operation

  • MODE_IGNORED(1)

不允许给定的调用者行目标 operation,但会静默失败(不应导致应用崩溃),…noThrow 类方法的返回值。实现层面一般是不执行请求的操作(不执行回调)或不返回任何数据或伪数据。

  • MODE_ERRORED (2)

不允许给定的调用方执行目标 operation,并且此意图应导致它出现致命错误,通常是 SecurityException。

  • MODE_DEFAULT (3)

调用者应使用自己默认的安全检查。这个模式很少使用,仅在 appop permission 场景使用,并且调用者必须显式检查并处理它。

noteOperation

noteOperation 和 noteProxyOperation 应当差不多,主要的差异应是 noteOperation 适用于非代理场景,因此只记录/检查目标 UID/PackageName 的 op,而不需要像 noteProxyOperation() 同时检查/记录代理方和被代理方。

另外,需要注意,由于会调用 verifyIncomingUid() 校验 UID 的合法性,该方法只允许应用记录/检查自己的 op,如果要跨应用记录/检查,必须获取 UPDATE_APP_OPS_STATS 特权

AppOpsService.java

1
2
3
4
5
6
7
8
9
10
    @Override
    public int noteOperation(int code, int uid, String packageName) {
        verifyIncomingUid(uid);
        verifyIncomingOp(code);
        String resolvedPackageName = resolvePackageName(uid, packageName);
        if (resolvedPackageName == null) {
            return AppOpsManager.MODE_IGNORED;
        }
        return noteOperationUnchecked(code, uid, resolvedPackageName, 0, null);
    }

noteProxyOperation

记录一个应用程序在处理 IPC 时代表另一个应用程序执行操作的过程。此函数将验证调用者(代理方) uid 和代理方包名称是否匹配,如果不匹配,则返回MODE_IGNORED。如果此调用成功,则代理应用程序和您的应用程序的操作的最后执行时间将更新为当前时间。

代理方调用,用以检查被代理方的 op

Android 10 的实现为例

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
    public int noteProxyOperation(int code, int proxyUid,
            String proxyPackageName, int proxiedUid, String proxiedPackageName) {
        verifyIncomingUid(proxyUid);
        verifyIncomingOp(code);
        String resolveProxyPackageName = resolvePackageName(proxyUid, proxyPackageName);
        if (resolveProxyPackageName == null) {
            return AppOpsManager.MODE_IGNORED;
        }
        final boolean isProxyTrusted = mContext.checkPermission(
                Manifest.permission.UPDATE_APP_OPS_STATS, -1, proxyUid)
                == PackageManager.PERMISSION_GRANTED;
        final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY
                : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY;
        final int proxyMode = noteOperationUnchecked(code, proxyUid,
                resolveProxyPackageName, Process.INVALID_UID, null, proxyFlags);
        if (proxyMode != AppOpsManager.MODE_ALLOWED || Binder.getCallingUid() == proxiedUid) {
            return proxyMode;
        }
        String resolveProxiedPackageName = resolvePackageName(proxiedUid, proxiedPackageName);
        if (resolveProxiedPackageName == null) {
            return AppOpsManager.MODE_IGNORED;
        }
        final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
                : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
        return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
                proxyUid, resolveProxyPackageName, proxiedFlags);
    }

其中入参 proxyXXX 是代理方(服务提供方)、proxiedXXX 是被代理方(调用服务的客户端)

该方法先执行访问控制,校验 proxyUid 的合法性(只允许应用创建 proxyUid 为自己 UID 的 op 记录,除非具有 UPDATE_APP_OPS_STATS 特权,才能跨应用记录),包括:

  • verifyIncomingUid():使用 Binder.getCallingUid 认证代理方 UID,要求等于调用 AppOpsService 者的 UID,否则检查调用者是否具有 UPDATE_APP_OPS_STATS 权限,该权限要求系统平台签名。也就是说某个 proxyUid 的记录只能由该 UID 对应的应用自己 note,除非具有特权。

注:从代码看,校验并没有检查 proxyUid 和 proxyPackageName 的匹配关系(也许在其它地方有校验),也没有对 proxiedUid/proxiedPackageName 做任何校验。

然后分别调用 noteOperationUnchecked() 检查(记录)代理方和被代理方的 app op,当代理方和被代理的 op mode 均为 MODE_ALLOWED 时,noteProxyOperation() 才返回 MODE_ALLOWED,任何一方 op 失败都会导致失败。

noteOperationUnchecked() 的具体实现不进一步展开描述,总体而言就是先查询 uid/packageName 对应记录的 op,然根据该 op 的 mode(MODE_ALLOWED 等)在 /data/system/appops.xml 创建对应记录,最后返回 mode 供调用方使用。

值得注意其中一行代码

1
final int switchCode = AppOpsManager.opToSwitch(code);

可以发现 op code 实际上会转换成 switch code 来最终执行访问控制。参考 OP 章节有关 switch code 的描述。

最后,noteProxyOperation() 调用者(代理方)需要根据返回的 mode 决定是否允许这次 operation 代理。

App Op 在系统中的记录

AppOpsService 在 /data/system/appops.xml 中记录各个应用的 op 状态和执行情况。

<uid> 标签记录

<uid> 标签记录指定应用各个 op 的状态模式,例如在 Android 10.0 某 UID 为 10113(com.example.service)记录如下:

1
2
3
<uid n="10115">
<op n="0" m="1" />
</uid>

其中 <op n="0" m="1" /> 表示一条 op 状态记录,n="0" 代表 op 的值,通过查阅代码可知对应 OP_COARSE_LOCATIONm=1 表示该 op 的 mode 为 MODE_IGNORED

检查该应用的权限声明,如下

1
2
3
4
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.aidlservice">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

当拒绝 Location 权限(”Deny“,默认情况)时对应的 op 状态:

1
<op n="0" m="1" />

注:这里 ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION 是两个不同的权限却对应只有一条 OP_COARSE_LOCATION 记录,是因为一些权限使用同一个 op 来控制,参考 OP 章节有关 switch code 的描述

当通过应用详情设置页面将 Location 权限切换为 ”Allow only while using the app“ (Android 10 专有) 时对应的 op 状态为:

1
<op n="0" m="4" />

这里 mode 4 是 Android 10 新引入的 MODE_FOREGROUND

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
     * Special mode that means "allow only when app is in foreground."  This is <b>not</b>
     * returned from {@link #unsafeCheckOp}, {@link #noteOp}, {@link #startOp}.  Rather,
     * {@link #unsafeCheckOp} will always return {@link #MODE_ALLOWED} (because it is always
     * possible for it to be ultimately allowed, depending on the app's background state),
     * and {@link #noteOp} and {@link #startOp} will return {@link #MODE_ALLOWED} when the app
     * being checked is currently in the foreground, otherwise {@link #MODE_IGNORED}.
     *
     * <p>The only place you will this normally see this value is through
     * {@link #unsafeCheckOpRaw}, which returns the actual raw mode of the op.  Note that because
     * you can't know the current state of the app being checked (and it can change at any
     * point), you can only treat the result here as an indication that it will vary between
     * {@link #MODE_ALLOWED} and {@link #MODE_IGNORED} depending on changes in the background
     * state of the app.  You thus must always use {@link #noteOp} or {@link #startOp} to do
     * the actual check for access to the op.</p>
     */
    public static final int MODE_FOREGROUND = 4;

<pkg> 标签记录

<pkg> 标签记录指定包名应用 op 的执行情况,即 op 执行日志,如下所示

1
2
3
4
5
6
7
8
<pkg n="com.example.service">
<uid n="10115" p="false">
<op n="1">
<st n="429496729604" t="1598412052901" r="1598412012847" />
<st n="1073741824004" r="1598411996821" />
</op>
</uid>
</pkg>

可以看到值为 1 的 op(OP_FINE_LOCATION )有两条记录,其中 n 是索引键值, t 表示访问时间戳,r 表示拒绝时间戳

关于日志格式定义,参考 AppOpsService.writeState

This post is licensed under CC BY 4.0 by the author.