前言
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 的包信息,这可以看做是包管理服务维护的一个已安装程序的核心数据库,这个数据库,随着每次应用安装、升级或卸载而进行更新。
主要属性如图:
标签内,定义了目前系统中的所有权限,分为系统内置的(package 属性为 android 的)和 apk 自定义的(package 属性为 apk 的包名)。根元素
含有每个 apk 的核心属性,以一个应用程序的条目为例:
-
Android 6.0 以下
packages.xml
-
Android 6.0 及以上
packages.xml
可以发现,其他信息没有太大差异,但是权限列表中,部分在 Android 6.0 被标记为高危的权限都不在
里了,如:READ_SMS
、READ_EXTERNAL_STORAGE
、CALL_PHONE
等。这是怎么一回事呢?
Android 6.0 之前,权限都是在安装时自动赋予的,不卸载应用的情况下,不能更改或撤销(实际上有些厂商打开了appOps,使得 6.0 以下也能更改权限,这种特殊情况我们不讨论)。而 Android 6.0 版本对 permission 的管理做了部分改动,针对 dangerous 级别,不再安装的时候赋予权限,而是在运行时动态申请。
我们大胆地推断下,packages.xml
里保留的是不会再变更的权限,运行时权限一定是另外单独地维护。我们在 data/system
目录下来了个全盘搜索,找到了 /data/system/users/0/runtime-permissions.xml
。
...
没错了,就是这个文件。里面就记录着运行时权限的授予和拒绝状态。申请时,申请的结果会动态修改 granted
值。
管理权限的仍然是 PMS,只不过 Android 6.0 之后新增了 runtime-permissions.xml
数据库。
权限赋予
我们知道,Android 应用安装时,会被分配一个唯一的 UID,应用启动时,包管理器会设置新建进程的 UID 和 GID 为应用程序的 UID。如果应用已经被赋予了额外的权限,就把这些权限映射成一组 GID,作为补充 GID 分配给进程。低层就可以依赖于进程的 UID、GID 和补充 GID 来决定是否赋予权限了。
那么,上面流程的重点:
- 权限是如何映射到 OS 层的 UID、GID 上的呢?
- 映射完是怎么分配给进程的?
- 低层是怎么判断是否赋予权限的?
我们一个个来看,首先第一个,权限是如何映射的?内置权限到 GID 的映射是定义在 /etc/permission/platform.xml
中。
···
···
值得注意的是:READ_EXTERNAL_STORAGE
这种运行时权限,在 Android 6.0 之后已经不会映射到 gid 了。动态赋予,动态申请,也就不需要映射了。
包管理器在启动时读取 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_r
和 android.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 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
里的
映射),如果 GrantedPermissions
类中的 grantedPermissions
集合包含目标权限,则检查通过。
如果没有该 GrantedPermissions
对象,则检查目标权限是否可以被自动授予,实际上 mSystemPermissions
就是 platform.xml
文件中的
标签映射缓存,记录了一些系统级应用的 uid 对应的 permission。例:
...
...
- 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 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 之后差异仅在于:危险级别权限可以动态修改授权情况,也就是修改 PermissionState
的 mGranted
值,所以每次权限执行,都会查询下 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 文件中设置
标签的 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
标签详细了解。
- 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 的和掘金,如果我的文章对你哪怕有一点点帮助,欢迎 ❤️!你的鼓励是我写作的最大动力!
最最重要的,请给出你的建议或意见,有错误请多多指正!