Android 权限管理 —— AppOps

Android 的安全保护源于权限,每个 App 需要申请权限才能使用特定的功能,用户在安装的时候可以浏览 App 已申请的权限再决定是否安装,不过一旦安装就相当于默认了可以使用该权限,所以这一功能基本属于鸡肋,后来便出现了权限管理这一安全防护:例如 360 手机卫士、安全管家、LBE一类第三方安全软件。除此之外,android 系统也并不是没有作为,在4.3系统以后出现了 AppOps 隐藏功能,能够针对 manifest 部分申请的权限进行一层限制,为什么说是隐藏功能呢?AppOps 本来有个可设置页面 AppOpsSummaryActivity,但是 Google 工程师一而再再而三的隐藏该界面,就是为了只让系统应用拥有后台修改 AppOps 的权限,不提供给其他用户或者第三方软件修改的机会。本文将从实例(修改短信权限)出发,分析 AppOps 的实现原理和鉴权过程。

1.短信Provider

Android 手机上,修改短信内容即是修改短信数据库,均是通过访问短信 Provider 的接口来实现的。该 Provider 是一个单独的 APK 实现的,一般来说在手机的 /system/app 目录下有一个叫做 TelephonyProvider.apk 的文件,该 Provider 实现是在 packages/providers/TelephonyProvider/src/SmsProvider.java

1.1 如何插入一条短信

本节简单展示一下如何在 Android 上插入一条短信:

private void insertSMS() {
    final String ADDRESS = "address";
    final String DATE = "date";
    final String READ = "read";
    final String STATUS = "status";
    final String TYPE = "type";
    final String BODY = "body";
    int MESSAGE_TYPE_INBOX = 1;
    int MESSAGE_TYPE_SENT = 2;
    ContentValues values = new ContentValues();
    /* 手机号 */
    values.put(ADDRESS, "13800138000");
    /* 时间 */
    values.put(DATE, "1281403142857");
    values.put(READ, 1);
    values.put(STATUS, -1);
    /* 类型1为收件箱,2为发件箱 */
    values.put(TYPE, 2);
    /* 短信体内容 */
    values.put(BODY, "测试插入一条短信");
    /* 插入数据库操作 */
*   Uri inserted = getContentResolver().insert(Uri.parse("content://sms"),
            values);
}

不瞒大家,为了节省时间,就随意摘了一段代码。。。但是效果都一样,相信大家在哪看大体都是这样,关键代码就是带*号的这一行。没错,记准了 Uri 是 content://sms 其实就够了。

1.2 插入流程

从 ContentResolver.insert 到 ContentProvider 的 Binder 调用过程在这里就不多说了,贴一小段代码足矣说明(ContentProviderNative.java):

public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
        throws RemoteException {
    try {
        switch (code) {
            ......

            case INSERT_TRANSACTION:
            {
                data.enforceInterface(IContentProvider.descriptor);
                String callingPkg = data.readString();
                Uri url = Uri.CREATOR.createFromParcel(data);
                ContentValues values = ContentValues.CREATOR.createFromParcel(data);

 *              Uri out = insert(callingPkg, url, values);
                reply.writeNoException();
                Uri.writeToParcel(reply, out);
                return true;
            }
            ......
        }
        ......
    }
    ......
}

发现 Binder 调用调的是 ContentProviderNative.insert,实际上就是 ContentProvider.Transport.insert 方法:

public Uri insert(String callingPkg, Uri uri, ContentValues initialValues) {
1.  if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) {
        return rejectInsert(uri, initialValues);
    }
    final String original = setCallingPackage(callingPkg);
    try {
2.      return ContentProvider.this.insert(uri, initialValues);
    } finally {
        setCallingPackage(original);
    }
}

步骤1:AppOps鉴权
步骤2:Provider实际插入操作

Provider的插入操作实现在这里就不讨论了,如果这一步里出现错误的话,返回的 Uri 将会是 null,如果顺利,将会返回 content://sms/{*}, 其中 {*} 指代插入的行数,该行数一定是大于0的,下面我们重点看鉴权过程。

1.3 鉴权过程

跟进 enforceWritePermission 方法:

private int enforceWritePermission(String callingPkg, Uri uri, IBinder callerToken)
                throws SecurityException {
1.  final int mode = enforceWritePermissionInner(uri, callingPkg, callerToken);
    if (mode != MODE_ALLOWED) {
        return mode;
    }

    if (mWriteOp != AppOpsManager.OP_NONE) {
2.      return mAppOpsManager.noteProxyOp(mWriteOp, callingPkg);
    }

    return AppOpsManager.MODE_ALLOWED;
}

步骤1:检查是否拥有该 Provider 声明需要的权限;
步骤2:检查通过 setAppOps 设置的 AppOps 权限;

我们直接来看第二步:
这一步既然是检查 setAppOps 设置的 AppOps 权限,那么是在哪里设置的呢?我们拿短信服务举例,即在 SmsProvider 的 onCreate 中:

@Override
public boolean onCreate() {
*   setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
    mOpenHelper = MmsSmsDatabaseHelper.getInstance(getContext());
    return true;
}

public final void setAppOps(int readOp, int writeOp) {
    if (!mNoPerms) {
        mTransport.mReadOp = readOp;
        mTransport.mWriteOp = writeOp;
    }
}

这个 OP_WRITE_SMS 就是写短信对应的 AppOps 权限了:

public static final int OP_WRITE_SMS = 15;

这个 15 就是写短信权限对应的 code,我们称之为 opCode,每种权限都会有一个 opCode。
回到第二步的 AppOpsManager.noteProxyOp 方法,它实际调的是 AppOpsService.noteProxyOperation() 方法:

@Override
public int noteProxyOperation(int code, String proxyPackageName,
        int proxiedUid, String proxiedPackageName) {
    verifyIncomingOp(code);
1.  final int proxyMode = noteOperationUnchecked(code, Binder.getCallingUid(),
            proxyPackageName, -1, null);
    if (proxyMode != AppOpsManager.MODE_ALLOWED || Binder.getCallingUid() == proxiedUid) {
        return proxyMode;
    }
2.  return noteOperationUnchecked(code, proxiedUid, proxiedPackageName,
            Binder.getCallingUid(), proxyPackageName);
}

其中 proxyPackageName 指的是服务提供方的包名,proxiedPackageName、proxiedUid 指的是服务调用方的包名和 Uid:
步骤1:检查服务提供方有权限与否
步骤2:检查服务调用方有权限与否
可以发现,AppOps 要求服务提供方和调用方均要有权限才行,我们回到短信验证的过程中来看,如果权限验证没过的分支执行的函数rejectInsert:


public Uri rejectInsert(Uri uri, ContentValues values) {
    // If not allowed, we need to return some reasonable URI.  Maybe the
    // content provider should be responsible for this, but for now we
    // will just return the base URI with a dummy '0' tagged on to it.
    // You shouldn't be able to read if you can't write, anyway, so it
    // shouldn't matter much what is returned.
    return uri.buildUpon().appendPath("0").build();
}

会发现并不会返回一个空或者直接报错,而是返回了一个 path 为 0 的合理 Uri。

2. AppOps

2.1 权限的存储

AppOps 是在 AMS 刚运行时开始初始化的,即它主要运行在 systemserver 进程:

public final class ActivityManagerService extends ActivityManagerNative
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    ...
    public void systemReady(final Runnable goingCallback) {
        ...
        mAppOpsService.systemReady();
        mSystemReady = true;
    }
    ...
}

每个 Uid 对应一套权限,存储在一个 SparseArray 中:

final SparseArray<UidState> mUidStates = new SparseArray<>();

UidState 是包含了针对 Uid 的权限和针对该 UID 下的应用的权限:

private static final class UidState {
    public final int uid;
    public ArrayMap<String, Ops> pkgOps; //针对该 UID 下的应用的权限
    public SparseIntArray opModes; //针对 Uid 的权限
    ...
}

每个包名对应一套权限,存储在 pkgOps 中,Key 是包名,Value 是对应的权限 Ops,Ops 是一个 SparseArray<Op>:

public final static class Ops extends SparseArray<Op> {
        public final String packageName;
        public final UidState uidState;
        public final boolean isPrivileged;
    ......
}

同时还会发现,Ops 类中也包含了 UidState,即拿到指定包名的权限,也能反查到该 Uid 的权限。Key 是权限对应的 Code,Value 就是该 code 对应的具体某一项权限,这里的 code 不是完全意义上的 opCode,而是 switchCode,它是 opCode 的子集,是压缩版的 opCode,原因是会出现一个权限开关能控制多种权限的现象,比如 OP_COARSE_LOCATION、OP_FINE_LOCATION、OP_GPS 三种权限实际上在设置权限的时候,先都会变成 OP_COARSE_LOCATION,即一个开关控制三种权限。

每当权限发生变化时,或者主动触发保存权限时,都会执行 AppOpsService.writeSate() 方法将权限写入 /data/system/appops.xml 中,举例如下:

<app-ops>
<pkg n="com.android.phone">
<uid n="1001" p="true">
<op n="4" t="1454047676885" />
<op n="11" t="1453514110695" />
<op n="14" t="1454050862950" />
<op n="15" m="0" t="1454050863238" />
<op n="23" t="1454047678100" />
<op n="40" t="1454052269904" d="4" />
<op n="45" t="1432781257792" d="2538" />
</uid>
</pkg>
</app-ops>

这里记录了一个包名为 com.android.phone 的应用的权限,其中 op 标签中,n 表示该项权限的 opCode,t 表示时间戳(毫秒),m 表示权限值 mode,有三种:


public static final int MODE_ALLOWED = 0;
public static final int MODE_IGNORED = 1;
public static final int MODE_ERRORED = 2;

如果没有 m 值,则为默认值,每种权限都有一种对应默认值,在 AppOpsManager.sOpDefaultMode 数组中,这是一个 int 数组,下标代表 opCode,内容代表默认权限值。其他属性值大家可以参考 writeState 方法一一对应过去。

2.2 权限验证

在第一节中我们已经知道,AppOps 通过 noteOperationUnchecked 函数去检查服务提供方和服务调用方有没有权限,在这里展开该函数来看:

private int noteOperationUnchecked(int code, int uid, String packageName,
        int proxyUid, String proxyPackageName) {
    synchronized (this) {
        Ops ops = getOpsLocked(uid, packageName, true);
        if (ops == null) {
            if (DEBUG) Log.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
                    + " package " + packageName);
            return AppOpsManager.MODE_ERRORED;
        }
        Op op = getOpLocked(ops, code, true);
1.      if (isOpRestricted(uid, code, packageName)) {
            return AppOpsManager.MODE_IGNORED;
        }
        if (op.duration == -1) {
            Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName
                    + " code " + code + " time=" + op.time + " duration=" + op.duration);
        }
        op.duration = 0;
        final int switchCode = AppOpsManager.opToSwitch(code);
        UidState uidState = ops.uidState;
        if (uidState.opModes != null) {
            final int uidMode = uidState.opModes.get(switchCode);
2.          if (uidMode != AppOpsManager.MODE_ALLOWED) {
                if (DEBUG) Log.d(TAG, "noteOperation: reject #" + op.mode + " for code "
                        + switchCode + " (" + code + ") uid " + uid + " package "
                        + packageName);
                op.rejectTime = System.currentTimeMillis();
                return uidMode;
            }
        }
        final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
3.      if (switchOp.mode != AppOpsManager.MODE_ALLOWED) {
            if (DEBUG) Log.d(TAG, "noteOperation: reject #" + op.mode + " for code "
                    + switchCode + " (" + code + ") uid " + uid + " package " + packageName);
            op.rejectTime = System.currentTimeMillis();
            return switchOp.mode;
        }
        if (DEBUG) Log.d(TAG, "noteOperation: allowing code " + code + " uid " + uid
                + " package " + packageName);
        op.time = System.currentTimeMillis();
        op.rejectTime = 0;
        op.proxyUid = proxyUid;
        op.proxyPackageName = proxyPackageName;
        return AppOpsManager.MODE_ALLOWED;
    }
}

步骤1:判断是否有其他用户限制,用在多用户账号切换场景上;
步骤2:判断 Uid 对应的指定权限;
步骤3:判断包对应的指定权限;

2.3 修改权限

2.3.1 举例:设置默认短信应用

既然 AppOps 做了种种权限限制,那么系统是如何管理、如何修改权限的呢?第三方能不能主动修改权限呢?在这里我们拿设置默认短信应用来举例。

在做修改、删除、管理短信相关的功能时,经常遇到一个权限问题,就是在 4.4 及以上的系统,由于 AppOps 的出现,“默认没有修改短信权限”的设定让这类功能瘫痪了,但同时发现设置了默认短信应用之后则功能又恢复有效了,这是为何呢?

经研究发现,在设置默认短信时,直接调用的实际上是 SmsApplication.setDefaultApplication() 方法:

public static void setDefaultApplication(String packageName, Context context) {
    TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
    if (!tm.isSmsCapable()) {
        // No phone, no SMS
        return;
    }

    final int userId = getIncomingUserId(context);
    final long token = Binder.clearCallingIdentity();
    try {
*       setDefaultApplicationInternal(packageName, context, userId);
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}

展开 setDefaultApplicationInternal:

private static void setDefaultApplicationInternal(String packageName, Context context,
        int userId) {
    // Get old package name
    String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(),
            Settings.Secure.SMS_DEFAULT_APPLICATION, userId);

    if (packageName != null && oldPackageName != null && packageName.equals(oldPackageName)) {
        // No change
        return;
    }

    // We only make the change if the new package is valid
    PackageManager packageManager = context.getPackageManager();
    Collection<SmsApplicationData> applications = getApplicationCollection(context);
    SmsApplicationData applicationData = getApplicationForPackage(applications, packageName);
    if (applicationData != null) {
        // Ignore OP_WRITE_SMS for the previously configured default SMS app.
        AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
        if (oldPackageName != null) {
            try {
                PackageInfo info = packageManager.getPackageInfo(oldPackageName,
                        PackageManager.GET_UNINSTALLED_PACKAGES);
1.              appOps.setMode(AppOpsManager.OP_WRITE_SMS, info.applicationInfo.uid,
                        oldPackageName, AppOpsManager.MODE_IGNORED);
            } catch (NameNotFoundException e) {
                Rlog.w(LOG_TAG, "Old SMS package not found: " + oldPackageName);
            }
        }

        // Update the secure setting.
        Settings.Secure.putStringForUser(context.getContentResolver(),
                Settings.Secure.SMS_DEFAULT_APPLICATION, applicationData.mPackageName,
                userId);

        // Configure this as the preferred activity for SENDTO sms/mms intents
        configurePreferredActivity(packageManager, new ComponentName(
                applicationData.mPackageName, applicationData.mSendToClass), userId);

        // Allow OP_WRITE_SMS for the newly configured default SMS app.
2.      appOps.setMode(AppOpsManager.OP_WRITE_SMS, applicationData.mUid,
                applicationData.mPackageName, AppOpsManager.MODE_ALLOWED);

        // Assign permission to special system apps
        assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                PHONE_PACKAGE_NAME);
        assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                BLUETOOTH_PACKAGE_NAME);
        assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                MMS_SERVICE_PACKAGE_NAME);
        assignWriteSmsPermissionToSystemApp(context, packageManager, appOps,
                TELEPHONY_PROVIDER_PACKAGE_NAME);
    }
}

步骤1:通过 setMode 函数将前一个默认短信应用的 OP_WRITE_SMS 权限设为 MODE_IGNORED;
步骤2:通过 setMode 函数将当前要设置的默认短信应用的 OP_WRITE_SMS 权限设为 MODE_ALLOWED;

可见,设置为默认短信应用之后就可以修改短信的原因在于 AppOps 权限的修改。

2.3.2 Android5.0 及以上设置权限

那么我们可否自己执行这个 setMode 函数呢?这里要分 Android 版本了(上文的代码均出自 6.0)我们先来看 5.0 - 6.0 (都一样)上的源码(AppOpsService.setMode是服务端实现,我们直接转到这里):

public void setMode(int code, int uid, String packageName, int mode) {
*   if (Binder.getCallingPid() != Process.myPid()) {
        mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
                Binder.getCallingPid(), Binder.getCallingUid(), null);
    }
    verifyIncomingOp(code);
    ......
}    

只看星号那一行即可,如果判断调用 setMode 的客户端不是本进程的话,则必须验证有 android.Manifest.permission.UPDATE_APP_OPS_STATS 权限,即非 systemserver 进程内部调用必须验证该权限:

<permission android:name="android.permission.UPDATE_APP_OPS_STATS"
        android:protectionLevel="signature|privileged|installer" />

这是签名验证的权限啊,所以第三方应用是无法添加该权限的~~~即6.0上第三方应用改 AppOps 就别想了。

2.3.3 Android 4.3 及 Android 4.4 上设置权限

同样来看4.3和4.4的 setMode 函数:

public void setMode(int code, int uid, String packageName, int mode) {
    verifyIncomingUid(uid);
    verifyIncomingOp(code);
    ......
}

private void verifyIncomingUid(int uid) {
*   if (uid == Binder.getCallingUid()) {
        return;
    }
    if (Binder.getCallingPid() == Process.myPid()) {
        return;
    }
    mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
            Binder.getCallingPid(), Binder.getCallingUid(), null);
}

注意带星的一行,只要传入的 uid 和调用者一致即可,明显就是直接可以调用了呀,这算是漏洞咩?
所以大家可以在自己的工程中加入如下代码:

if ( 19 >= Build.VERSION.SDK_INT) {
    try {
        AppOpsManager appOpsManager = (AppOpsManager) sContext.getSystemService("appops");
        appOpsManager.setMode(15, android.os.Process.myUid(), packageName, 0);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

就能解决一部分没有深度定制的 rom 以及官方 rom 上修改短信没权限的问题。

3. 小结

本文基于短信修改权限对 AppOps 作了一定深度上的分析,并基于4.3、4.4的源码对这个直接修改AppOps的“漏网之鱼”作了介绍。

在这里感谢师弟@张晓的支持~~~~

标签: none

添加新评论