「Android 安全架构」你真的了解权限机制吗?

96
FeelsChaotic
0.3 2018.05.26 17:49* 字数 5624

前言

Android将安全设计贯穿系统架构的各个层面,覆盖系统内核、虚拟机、应用程序框架层以及应用层各个环节,力求在开放的同时,也最大程度地保护用户的数据、应用程序和设备的安全。Android安全模型主要提供以下几种安全机制:

从技术架构角度来看,Android安全模型基于Linux操作系统内核安全性,有基本的用户和文件访问隔离,在这个基础上辅以内存管理技术和进程间通信机制,来适应移动端处理器性能与内存容量的限制。在应用层面,使用显式定义且经用户授权的权限控制机制,系统化地规范并强制各类应用程序的行为准则与权限许可,可以看到权限机制在安全模型中所处的位置。

那么为什么有权限机制?

我们知道 Android 应用程序是沙箱隔离的,每个应用都有一个只有自己具有读写权限的专用数据目录,应用只能访问自己的文件和一些设备上全局可访问的资源。

那如果我需要访问系统服务呢?这就有了 Android 的权限机制。所以根本原因一是沙箱隔离,二是服务支持的需求。

在本文开始之前,先抛出几个问题思考,什么是权限?权限是怎么进行赋予的?怎么判断一个组件是否拥有特定的权限?权限维护在哪?Android 6.0 运行时权限是什么原理?权限赋予后还能再更改吗?

带着问题和结构图,我们一一解开疑惑。

权限的本质

什么是权限?

在 Android 中,一个权限,本质上是一个字符串,一个可以表示执行特定操作的能力的字符串。比如说:访问 SD 卡的能力,访问通讯录的能力,启动或访问一个第三方应用中的组件的能力。

使用 pm list permissions -f 命令可以详细查看 Android 所有预定义的权限。我们挑一个权限出来:

+ permission:android.permission.DELETE_PACKAGES
  package:android
  label:null
  description:null
  protectionLevel:signature|privileged

可以看到,一个权限的信息包括:定义的包名、标签、描述和保护级别,保护级别,嗯,这个我们需要详细讲讲。因为不是应用声明了权限,就一定会全部被自动赋予的,保护级别决定了包管理器是否应该赋予组件所申请的权限。

权限的级别

  • normal 级别
    权限保护级别的默认值,无须用户确认,只要声明了,就自动默默授权。如:ACCESS_NETWORK_STATE。

  • dangerous 级别
    赋予权限前,会弹出对话框,显式请求权限。如:READ_SMS。因为 Android 需要在安装时赋予权限,所以安装的确认对话框,也会显示列出权限清单。

  • signature 级别
    signature 级别的权限是最严格的权限,只会赋予与声明权限使用相同证书的应用程序。

    以系统内置 signature 级别权限为例,Android 系统应用的签名由平台密钥签发,默认情况下源码树里有 4 个不同的密钥文件:platform、shared、media 和 testkey。所有核心平台的包(如:设置、电话、蓝牙)均使用 platform 密钥签发;搜索和通讯录相关的包使用 shared 签发;图库和媒体相关的包使用 media 密钥签发;其他的应用使用 testkey 签发。定义系统内置权限的 framework-res.apk 文件是使用平台密钥签发的,因此任何试图请求 signature 级别内置权限的应用程序,需要使用与框架资源包相同的密钥进行签名。

  • signatureOrSystem 级别
    可以看做是一种折中的级别,可被赋予与声明权限具有相同签名证书密钥的应用程序(同 signature 级别)或者系统镜像的部分应用,也就是说这允许厂商无须共享签名密钥。Android 4.3 之前,安装在 system 分区下的应用会被自动赋予该保护级别的权限,而 Android 4.4 之后,只允许安装在 system/priv-app/ 目录下的应用才能被主动赋予。

权限管理

那么,系统内置权限,自定义权限,是怎么维护和管理的?

在每个应用安装时,权限就已经赋予了,系统使用包管理服务来管理权限。打开我们系统目录下的 /data/system/packages.xml,可以看到文件包含了所有已定义的权限列表和所有 apk 的包信息,这可以看做是包管理服务维护的一个已安装程序的核心数据库,这个数据库,随着每次应用安装、升级或卸载而进行更新。

主要属性如图:

<permissions> 标签内,定义了目前系统中的所有权限,分为系统内置的(package 属性为 android 的)和 apk 自定义的(package 属性为 apk 的包名)。根元素 <package> 含有每个 apk 的核心属性,以一个应用程序的条目为例:

  • Android 6.0 以下 packages.xml

    <package 
        name="com.feelschaotic.demo" 
        codePath="/data/app/com.feelschaotic.demo-1" 
        nativeLibraryPath="/data/app/com.feelschaotic.demo-1/lib" 
        primaryCpuAbi="x86" 
        flags="5783366" 
        ft="16349bcc4d0" 
        it="16349bcc752" 
        ut="16349bcc752" 
        version="8220" 
        userId="10097">
        <sigs count="1">
            <cert index="7" />
        </sigs>
        <perms>
            <item name="android.permission.READ_SMS" />
            <item name="android.permission.ACCESS_FINE_LOCATION" />
            <item name="android.permission.CHANGE_NETWORK_STATE" />
            <item name="android.permission.INTERNET" />
            <item name="android.permission.READ_EXTERNAL_STORAGE" />
            <item name="android.permission.ACCESS_COARSE_LOCATION" />
            <item name="android.permission.READ_PHONE_STATE" />
            <item name="android.permission.CALL_PHONE" />
            <item name="android.permission.CHANGE_WIFI_STATE" />
            <item name="android.permission.ACCESS_NETWORK_STATE" />
            <item name="android.permission.CAMERA" />
            <item name="android.permission.WRITE_EXTERNAL_STORAGE" />
            <item name="android.permission.READ_CONTACTS" />
        </perms>
        <proper-signing-keyset identifier="1686" />
        <signing-keyset identifier="1686" />
    </package>
  • Android 6.0 及以上 packages.xml

 <package 
    name="com.feelschaotic.demo" 
    codePath="/data/app/com.feelschaotic.demo-Gi5ksdF6mUDLakfOugCcwQ==" 
    nativeLibraryPath="/data/app/com.feelschaotic.demo-Gi5ksdF6mUDLakfOugCcwQ==/lib" 
    primaryCpuAbi="x86" 
    publicFlags="945307462" 
    privateFlags="0" 
    ft="16348dc3870" 
    it="16343f1d6aa" 
    ut="16348dc4c4d" 
    version="8220" 
    userId="10102">
        <sigs count="1">
            <cert index="20" key="..." />
        </sigs>
        <perms>
          <!-- 此处普通权限的 granted 全都默认是 true,且不可改变 granted 值-->
            <item name="android.permission.CHANGE_NETWORK_STATE" granted="true" flags="0" />
            <item name="android.permission.INTERNET" granted="true" flags="0" />
            <item name="android.permission.CHANGE_WIFI_STATE" granted="true" flags="0" />
            <item name="android.permission.ACCESS_NETWORK_STATE" granted="true" flags="0" />
        </perms>
        <proper-signing-keyset identifier="48" />
    </package>

可以发现,其他信息没有太大差异,但是权限列表中,部分在 Android 6.0 被标记为高危的权限都不在 <perms> 里了,如:READ_SMSREAD_EXTERNAL_STORAGECALL_PHONE 等。这是怎么一回事呢?

Android 6.0 之前,权限都是在安装时自动赋予的,不卸载应用的情况下,不能更改或撤销(实际上有些厂商打开了appOps,使得 6.0 以下也能更改权限,这种特殊情况我们不讨论)。而 Android 6.0 版本对 permission 的管理做了部分改动,针对 dangerous 级别,不再安装的时候赋予权限,而是在运行时动态申请。

我们大胆地推断下,packages.xml 里保留的是不会再变更的权限,运行时权限一定是另外单独地维护。我们在 data/system 目录下来了个全盘搜索,找到了 /data/system/users/0/runtime-permissions.xml

<pkg name="com.feelschaotic.demo">
   <!-- 该demo我们故意拒绝了定位权限,可以看到:ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 的 granted 为 false -->
    <item name="android.permission.ACCESS_FINE_LOCATION" granted="false" flags="1" />
    <item name="android.permission.READ_EXTERNAL_STORAGE" granted="true" flags="0" />
    <item name="android.permission.ACCESS_COARSE_LOCATION" granted="false" flags="1" />
    <item name="android.permission.READ_PHONE_STATE" granted="true" flags="0" />
    <item name="android.permission.WRITE_EXTERNAL_STORAGE" granted="true" flags="0" />
    ...
</pkg>

没错了,就是这个文件。里面就记录着运行时权限的授予和拒绝状态。申请时,申请的结果会动态修改 granted 值。

管理权限的仍然是 PMS,只不过 Android 6.0 之后新增了 runtime-permissions.xml 数据库。

权限赋予

我们知道,Android 应用安装时,会被分配一个唯一的 UID,应用启动时,包管理器会设置新建进程的 UID 和 GID 为应用程序的 UID。如果应用已经被赋予了额外的权限,就把这些权限映射成一组 GID,作为补充 GID 分配给进程。低层就可以依赖于进程的 UID、GID 和补充 GID 来决定是否赋予权限了。

那么,上面流程的重点:

  1. 权限是如何映射到 OS 层的 UID、GID 上的呢?
  2. 映射完是怎么分配给进程的?
  3. 低层是怎么判断是否赋予权限的?

我们一个个来看,首先第一个,权限是如何映射的?内置权限到 GID 的映射是定义在 /etc/permission/platform.xml 中。

<permissions>
    ···
    <permission name="android.permission.READ_EXTERNAL_STORAGE" >
        <group gid="sdcard_r" />
    </permission>

    <permission name="android.permission.WRITE_EXTERNAL_STORAGE" >
        <group gid="sdcard_r" />
        <group gid="sdcard_rw" />
    </permission>
    
    <permission name="android.permission.INTERNET" >
        <group gid="inet" />
    </permission>
    ···
</permissions>

值得注意的是:READ_EXTERNAL_STORAGE 这种运行时权限,在 Android 6.0 之后已经不会映射到 gid 了。动态赋予,动态申请,也就不需要映射了。

    <!-- These are permissions that were mapped to gids but we need
         to keep them here until an upgrade from L to the current
         version is to be supported. These permissions are built-in
         and in L were not stored in packages.xml as a result if they
         are not defined here while parsing packages.xml we would
         ignore these permissions being granted to apps and not
         propagate the granted state. From N we are storing the
         built-in permissions in packages.xml as the saved storage
         is negligible (one tag with the permission) compared to
         the fragility as one can remove a built-in permission which
         no longer needs to be mapped to gids and break grant propagation. -->
         
<permission name="android.permission.READ_EXTERNAL_STORAGE" />
<permission name="android.permission.WRITE_EXTERNAL_STORAGE" />

包管理器在启动时读取 platform.xml,并维护「权限-GID」对应的列表。当它给安装中的包授权时,会把权限对应的 GID 加入到该应用进程的补充 GID 中。

我们举个例子,看下进程的属性:adb shell ps 拿到想要查找的进程的 PID

USER    PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
...
u0_a88  2149  1624 1929916 112340 SyS_epoll_wait      0 S com.feelschaotic.demo

接着 adb shell -> cd proc -> cd 2149 (2149 为进程 PID,不固定) -> cat status

我们关注如下两行:

Gid: 10050 10050 10050 10050
Groups: 1006 1015 1028 3002 3003 50050

这里我们便看到了系统进程的权限配置信息,这里的数字具体代表意义,可以在Android
\system\core\include\private\android_filesystem_config.h 里面看到,其部分内容如下:

#define AID_CAMERA 1006  /* camera devices */  
#define AID_SDCARD_RW 1015 /* external storage write access */
#define AID_SDCARD_R 1028 /* external storage read access */
···
static const struct android_id_info android_ids[] = {
{ "camera",    AID_CAMERA, },
{ "sdcard_r", AID_SDCARD_R, },
{ "sdcard_rw", AID_SDCARD_RW, },
    ···
};

有没有发现什么?前文所述 sdcard_randroid.permission.READ_EXTERNAL_STORAGE 的映射已经定义在 /etc/permission/platform.xml 中了,此处 sdcard_r 映射 AID_SDCARD_R 对应 gid = 1015,这就是权限映射到 gid 的整个关系。

总共分为三层:

那么第二个问题,当我们安装应用完成,启动应用,应用的进程是如何启动并被赋予进程属性的呢?

每个应用都会运行在自己的 Dalvik 虚拟机进程中,但是为了提高启动效率,Android 不会为每个应用都新建一个 Dalvik 进程,而是采用 fork 的形式。每个进程都 fork form zygote 进程。

那么 fork 比起 new ,效率提高在哪里?

因为 zygote 进程已经预加载了大部分核心和 Java 应用框架库,fork 的子进程会继承 zygote 的进程空间,也就是说,fork 的子进程,可以共享这些预加载的副本(记住,是副本,不是直接共享。fork 时,会 copy-on-write 预加载的内容),减少了重新加载核心库的时间。

当 zygote 收到启动新进程的请求时,它会 fork 自身出一个子进程,并对该子进程做特殊化处理。其源代码位于 dalvik/vm/native/dalvik_system_Zygote.c 中。forkAndSpecializeCommon() 的主要代码如下:

static pid_t forkAndSpecializeCommon(const u4* args, boolisSystemServer)  
{  
 ...  
 pid = fork();  //创建新进程  
 if (pid == 0)  //判断是否是root,有没有权限修改自己的进程属性
 {  
  setgroupsIntarray(gids);  //设置进程的所有组  
  setrlimitsFromArray(rlimits);  
  setgid(gid);    //设置进程的组ID  
  setuid(uid);    //设置进程的用户ID  
     }  
  ...  
} 

如上所示:这里设置进程的组 ID 和用户 ID,通过 fork 创建的子进程调用 setgroups Intarray 设置该进程所属的组,这样应用程序就拥有了该组的权限,并且可以通过 setgid()setuid() 确定应用程序的 GID 及 UID 值。刚刚开始 fork 时,子进程是以 root 执行的,所以它可以更改自己的进程属性,当属性都设置完成,子进程就以分配的 GID 和 UID 执行,此时,子进程无法再更改自己的进程属性了,因为用户 ID 已经不是 root 即 ! = 0 了,没有修改自己进程属性的权限了。

adb shell ps,看下进程列表:

USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME

root             1     0   10456   2352 SyS_epoll_wait      0 S init
...
root          1620     1 1614572  20312 poll_schedule_timeout 0 S zygote
...
u0_a88        3468  1620 1929916 112340 SyS_epoll_wait      0 S com.feelschaotic.demo
u0_a90        3574  1620 1696012  24176 SyS_epoll_wait      0 S com.demo.pushdemo
u0_a90        3607  1620 1708844  26748 SyS_epoll_wait      0 S com.demo.pushdemo:mult
u0_i0         3741  1879 1557800  11720 SyS_epoll_wait      0 S com.android.chrome:sandboxed
u0_a15        3774  1620 1690604  22056 SyS_epoll_wait      0 S com.google.android.ext.services
system        3805  1620 1687840  19688 SyS_epoll_wait      0 S com.android.keychain
u0_a42        3831  1620 1834408  45660 SyS_epoll_wait      0 S com.android.chrome
...

PID 表示应用的进程 ID,PPID 表示父进程 ID,NAME 表示进程名称(一般情况下 NAME 是应用包名)。可以看到 zygote 进程是由 init 进程启动,所有的应用进程的父进程都是 zygote。USER 表示的是进程的专有用户,这个我们下次再详细讲讲 Android 的用户管理机制。

好了,既然如此,每个应用进程都分配好自己的 GID、UID和补充 GID,系统内核和守护进程就可以用这些标识来决定,是否要赋予进程权限。

权限检查(权限执行)

1. 系统内核层权限检查

思考一下:如果我们的应用没有在 AndroidManifest.xml 中申请 android.permission.INTERNET 权限就进行网络请求,是不是会报 Permission denied 错误。这个权限,是谁来检查?其他进程来检查吗?明显不是,网络访问权限是由低层来进行控制的。

Android 的访问控制,和 Linux 是一样的,但 Android 增加了个特有的网络访问安全控制机制,也就是说,创建网络套接字的进程,必须属于 inet 组。

如上内核代码,current_has_network(void) 方法检查了进程的所在组。如果不在 inet 组,则直接返回错误。所以为了使我们的应用具有访问网络的能力,我们需要在 AndroidManifest.xml 中申请 INTERNET 权限,经过解析,逐步映射到内核层的组 ID 和用户 ID,最终才能通过内核层的检查。

你可能会有疑问,那非内核层的其他 C/C++ 层,要怎么拿到进程的所在组信息呢?

在 PMS 初始化所有包信息之后,就会调用 mSettings.writeLPr()。

这段代码的任务就是将mPackages 中保存的所有包的信息保存到 /data/system/packages.list。所以,packages.list中保存了所有应用申请的权限,C代码只要读这个文件就能判断某个应用是否申请了我们要求的权限。

2. 框架层

因为 Android 6.0 之前组件不能在运行时改变权限,所以系统的权限检查执行过程是静态的。这个情况下,组件的角色和权限的等安全属性会被放置在元数据中,即 AndroidManifest.xml 文件中,而不是组件的本身。系统包管理器会负责记录组件的权限,所以静态权限检查可以从包管理器拿到权限,由运行环境或容器来执行权限检查,这样子可以把业务逻辑和安全决策分离开来,但是灵活性不足。

那 Android 组件可不可以不预先声明权限在 AndroidManifest.xml 中呢?答案是:可以的。Android 的动态权限执行,可以让组件自身执行权限检查,而不是运行环境。

所以接下来我们将深入了解框架层的动态和静态权限执行的原理。

动态权限执行

动态权限执行,最典型的场景,就是 IPC。Android 的核心系统服务统一会注册到服务管理器,任何应用,只要知道服务的注册名称,就可以拿到对应的 Binder引用,就可使用 Binder IPC 机制调用服务。因为 Binder 没有内置的访问控制机制,所以每个系统服务需要自己实现访问控制机制。

系统服务可以直接检查调用者的 UID,通过限定 UID 来控制访问权限,这种方式简单直接,但是对于非固定UID的应用,就比较棘手了。而且大部分服务,并不关心调用者的 UID,只需要检查调用者是否被赋予特定的权限即可。所以这种方式,比较适合只允许以 root(UID:0) 或 system(UID:1000) 运行的进程访问的服务检查。

那换一种方式,服务怎么拿到调用者的权限列表?我们知道,大部分 UID 都是和包一一对应的,除了共享 UID。(共享 UID 后面再详细解释)

使用 Binder.getCallingUid()Binder.getCallingPid() 获取调用者的 UID 和 PID,通过 UID 在包管理器中查询到对应应用的权限。android.content.Context 类中就有 checkPermission(String permission, int pid, int uid) 方法。实质上会调用到 PMS 中的 checkUidPermission(String perName, int uid),如下:

  • Android 6.0 以下 PMS 中的 checkUidPermission(String perName, int uid)
    public int checkUidPermission(String permName, int uid) {
        synchronized (mPackages) {
            Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
            if (obj != null) {
                GrantedPermissions gp = (GrantedPermissions)obj;
                if (gp.grantedPermissions.contains(permName)) {
                    return PackageManager.PERMISSION_GRANTED;
                }
            } else {
                HashSet<String> perms = mSystemPermissions.get(uid);
                if (perms != null && perms.contains(permName)) {
                    return PackageManager.PERMISSION_GRANTED;
                }
            }
        }
        return PackageManager.PERMISSION_DENIED;
    }

Android 6.0 以下的 checkUidPermission() 方法比较简单,首先,基于入参 uid 获取应用的 appId,拿到权限列表对象(也就是 packages.xml 里的 <package> 映射),如果 GrantedPermissions 类中的 grantedPermissions 集合包含目标权限,则检查通过。

如果没有该 GrantedPermissions 对象,则检查目标权限是否可以被自动授予,实际上 mSystemPermissions 就是 platform.xml 文件中的 <assing-permission> 标签映射缓存,记录了一些系统级应用的 uid 对应的 permission。例:

...
<assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" />
<assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="graphics" />
...
  • Android 6.0 及以上 PMS 中的 checkUidPermission(String perName, int uid)
 @Override
    public int checkUidPermission(String permName, int uid) {
        final int callingUid = Binder.getCallingUid();
        final int callingUserId = UserHandle.getUserId(callingUid);
        final boolean isCallerInstantApp = getInstantAppPackageName(callingUid) != null;
        final boolean isUidInstantApp = getInstantAppPackageName(uid) != null;
        final int userId = UserHandle.getUserId(uid);
        if (!sUserManager.exists(userId)) {
            return PackageManager.PERMISSION_DENIED;
        }

        synchronized (mPackages) {
            Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
            if (obj != null) {
                ...
                final SettingBase settingBase = (SettingBase) obj;
                final PermissionsState permissionsState = settingBase.getPermissionsState();
                if (permissionsState.hasPermission(permName, userId)) {
                    if (isUidInstantApp) {
                        BasePermission bp = mSettings.mPermissions.get(permName);
                        if (bp != null && bp.isInstant()) {
                            return PackageManager.PERMISSION_GRANTED;
                        }
                    } else {
                        return PackageManager.PERMISSION_GRANTED;
                    }
                }
                ...
            } else {
                ArraySet<String> perms = mSystemPermissions.get(uid);
                if (perms != null) {
                    if (perms.contains(permName)) {
                        return PackageManager.PERMISSION_GRANTED;
                    }
                    ...
                }
            }
        }

        return PackageManager.PERMISSION_DENIED;
    }

可以注意到,6.0 之后 checkPermission() 方法有所改变。多了从 mSettings.mPermissions 去查询权限列表。

关键就在于这个 mSettings 里面保存的这个 SettingBase 对象,它记录了 PermissionsState 也就是权限的授予情况。

// PermissionsState.java
public boolean hasPermission(String name, int userId) {
    enforceValidUserId(userId);

    if (mPermissions == null) {
        return false;
    }

    PermissionData permissionData = mPermissions.get(name);
    return permissionData != null && permissionData.isGranted(userId);
}

所以检查权限的流程是本来就有的,6.0 之后差异仅在于:危险级别权限可以动态修改授权情况,也就是修改 PermissionStatemGranted 值,所以每次权限执行,都会查询下 mGranted 值。

静态权限执行

静态权限执行的典型场景,是跨应用组件交互。

我们使用隐式 Intent 来表达意图,搜索匹配的组件,如果有多个,弹出选择框,目标组件被选定后,会由 ActivityManagerService 执行权限检查,检查目标组件是否有相应的权限要求,如果有,则把权限检查的工作交给 PMS,去检查调用者有没有被授权这些权限。

接下来的总体的流程和动态执行流程大致相同:Binder.getCallingUid()Binder.getCallingPid()获取调用者的 UID 和 PID,然后利用 UID 映射包名,再获得相关权限集合。如果权限集合中含有所需权限即启动,否则抛出 SecurityException 异常。

静态权限执行这里,我们可以详细了解下,每种组件的权限检查时机和具体顺序是怎么样的。

组件权限执行

思考一下,什么时候会执行对调用者的权限检查?那肯定是在目标组件被调用的时候,去解析目标组件声明的权限,如果有,就执行权限检查。

  • Activity 和 Service

Activity 显而易见,会在 startActivity()startActivityForResult() 里解析到声明权限的 Activity 时,就执行权限检查。

而 Service startService()stopService()bindService(),这 3 个方法被调用时都会进行权限检查。

  • 广播

我们注意到,发送广播除了常用的 sendBroadcast(Intent intent),还有个 sendBroadcast(Intent intent, String receiverPermission),该方法可以要求广播接受者具备特定的权限,但是,调用 sendBroadcast 是不会进行权限检查的,因为广播是异步的,所以权限检查会在 intent 传递到已注册的广播接受者时进行,如果接收者不具备特定的权限,则不会接收到该广播,也不会收到 SecurityException 异常。

反过来,接收者可以要求广播发送者必须具备的权限,所要求的权限在 manifest 文件中设置 <receiver> 标签的 permission 属性,或者动态注册时指定 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler),权限检查也是在广播传递时执行。

所以,收发广播可以分开指定权限。值得一提的是,一些系统广播被声明为 protected,并且只能由系统进程发送,比如 PACKAGE_INSTALLED。只能由系统进程发送,这个限制会在内核层进行检查,对调用者的 UID 进行匹配,只能是 SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID 或 root。如果其他 UID 的进程试图发送系统广播,则会收到 SecurityException 异常。

想了解所有的系统广播,可以打开/system/framework/framework-res.apk 中的 AndroidManifest.xml <protected-broadcast> 标签详细了解。

  • ContentProvider

ContentProvider 可以为读写分别指定不同的权限,即:调用目标 provider、query() 方法 和 insert()update()delete() 都会进行权限检查。

3. 总结

综上所述,Android 的权限的检查会在各个层次上实施。

高层的组件,例如应用和系统服务,通过包管理器查询应用程序被赋予的权限,并决定是否准予访问。

低层的组件,通常不访问包管理器,比如本地守护进程,依赖于进程的 UID、GID 和补充 GID 来决定赋予。

访问系统资源时,如设备文件、UNIX 域套接字和网络套接字,则由内核根据所有者、目标资源的访问权限和访问进程的进程属性或者 packages.list 来进行控制。

共享 UID

最后简单说下共享 UID,填一下前面挖的坑。虽说 Android 会为每一个应用分配唯一的 UID,但如果应用使用相同的密钥签发,就可以使用相同 UID 运行,也就是运行在同一个进程中。

这个特性被系统应用和核心框架服务广泛使用,比如:Google Play 和 Google 定位服务,请求同一进程内的 Google 登录服务,从而达到静默自动同步用户数据的体验。

值得注意的是:Android 不支持将一个已安装的应用,从非共享 UID 切换到共享状态,因为改变了已安装应用的 UID,会导致应用失去对自己文件的访问权限(在一些早期 Android 版本中),所以如果使用共享 UID 必须从一开始就设计好。


我是 FeelsChaotic,一个写得了代码 p 得了图,剪得了视频画得了画的程序媛,致力于追求代码优雅、架构设计和 T 型成长。

欢迎关注 FeelsChaotic 的简书掘金,如果我的文章对你哪怕有一点点帮助,欢迎 ❤️!你的鼓励是我写作的最大动力!

最最重要的,请给出你的建议或意见,有错误请多多指正!

Android进阶之路
Web note ad 1