详细分析Android权限机制实现,分析APP组件、Android框架层、系统服务、原生守护进程的权限控制实现
Android APP运行在受限沙箱内,为了完成与其它APP或系统的交互,需要申请额外权限。权限在APP安装时被授权给应用,且在APP生命周期内保持不变。权限可以被映射为Linux补充GID,用于内核在授权访问系统资源时进行权限检查。
Binder文中讲过,APP与其它APP、系统服务间通信使用Binder的IPC机制。服务端可以通过getCallingUid()获取APP的UID,并通过查找包管理器数据库中保存的权限,来执行权限检查。
与组件关联的权限,在APP的Manifest文件中声明,并由系统自动执行,但是APP也可以选择额外的动态检查。除了使用内置权限,APP还可以自定义权限,并将它们与组件相关联,从而用于组件的访问控制。
一、APP权限申请与管理
APP在AndroidManifest.xml文件中,使用 标签来申请权限,通过 标签来自定义权限。
--snip--
上述Manifest的权限申请中,其实隐含了两处关键信息:
1)权限分类
Android权限命名,是包名+permission字符串+具体权限名的形式。其中包名是android的代表系统内置权限,如上面的android.permission.READ_SMS权限。而非android包定义的则是自定义权限。自定义权限包括系统预装及用户安装应用定义的权限,比如上面com.android.launcher.permission.READ_SETTINGS 权限是launcher系统应用的自定义权限。
所以Android的权限有两种:内置权限和自定义权限
2)权限保护级别
(访问Accounts Service服务中的帐户列表)
(读取短信)
上述2个均为系统内置权限,但我们发现在APP安装时,GET_ACCOUNTS的权限申请系统会默认允许,而READ_SMS权限则要用户手动确认。这是因为这两个权限拥有不同的权限保护级别。
Android系统上有4种不同的权限保护级别,以下分别介绍:
1、normal 级别
这是权限保护级别的默认值,它定义了访问系统或其它APP的低风险权限。normal级别的权限无需用户确认,会自动授权。例如上面的GET_ACCOUNTS权限。
2、dangerous 级别
dangerous 级别的权限可以访问用户数据或某种程序的控制用户设备。例如READ_SMS允许应用读取手机短信,因此在赋予dangerous 的权限前,Android会弹出一个确认对话框显示请求信息,并由用户手动确认。
3、signature 级别
signature 级别的权限只会授权给那些与权限声明者使用相同证书的APP。这是最严格的权限级别,因为申请这个权限你得持有APP或平台的签名私钥。
4、signatureOrSystem 级别
这是一种折中的方案,权限可被赋予系统镜像的部分应用或拥有相同签名的应用。在4.4版本之后,安装在/system/priv-app/目录下的应用,才能被赋予这个保护权限。
拿到一个内置权限,如果查看它的保护级别呢?
我们知道Android系统内置权限定义在以android开头的包中,这些包的集合其实就是平常说的Android 框架层。Android 框架的核心是一组由系统服务共享的类,这些类被打包成JAR文件放到/system/framework/目录下。到这个目录下可以发现,除了JAR文件,Android框架还包含了一个framework-res.apk的APK文件。
所有的内置权限,都在APK的AndroidManifest.xml文件中定义,搜索上面申请的2个权限GET_ACCOUNTS和READ_SMS,它们的权限保护级别分别是normal和dangerous,这也是上面一个需要用户手动确认,一个不需要的原因:
每个APP权限,是通过一个名叫包管理器的系统服务进行管理的。包管理器维护着一个已安装APP的核心数据库,其中包括安装路径、版本、签名证书和每个包的权限,另外还包含了一个所有已定义权限列表。这个核心数据库以XML文件的形式保存于/data/system/packages.xml。
以网易云音乐为例,看一下packages.xml中的应用程序条目:
每个APP包都用 标签来表示,包含UID(userId属性)、签名证书( 标签)和所授权限( 下的子标签中)。
使用android.content.pm.PackageManager类的getPackageInfo()方法,即可获取 标签下所有信息,比如经常使用的APK签名校验。
以上基于包管理器的权限机制,对Android高层组件,如APP、framework和系统服务已经足够,它们通过包管理器查询APP被赋予哪些权限,并决定是否允许访问。
对于低层的组件,如原生守护进程,通常不访问包管理器,而依赖于进程的UID、GID和补充GID来决定是否允许访问。而访问系统资源,如设备文件、UNIX域套接字和网络套接字,则由内核根据资源所有者、目标资源的访问权限和访问进程的UID和GID来控制。
关于上面提到的高层和低层组件,参考下图Android架构:
二、权限到UID/GID映射及权限执行
前面介绍,低层的组件如原生守护进程,或内核控制的一些系统资源的访问,其权限控制通常并不访问包管理器,而是通过UID、GID和补充GID来进行授权。
这就需要将权限映射成相应的UID、GID,并不是所有的权限都会被映射,所以被映射的权限都定义在/etc/permissions/platform.xml文件的 标签中:
--snip--
--snip--
注释很关键,描述了映射原因。这里只有权限名到组名的映射关系,Android系统中没有/etc/group文件,组名到GID的映射是静态的,定义在android_filesystem_config.h文件中,可以在aosp源码中找到,我们以网易云音乐的READ_EXTERNAL_STORAGE权限为例,如下:
#define AID_SDCARD_R 1028 /* external storage read access */
#define AID_CLAT 1029 /* clat part of nat464 */
#define AID_LOOP_RADIO 1030 /* loop radio devices */
#define AID_MEDIA_DRM 1031 /* MediaDrm plugins */
static const struct android_id_info android_ids[] = {
……
{ "sdcard_r", AID_SDCARD_R, },
{ "clat", AID_CLAT, },
{ "loop_radio", AID_LOOP_RADIO, },
{ "mediadrm", AID_MEDIA_DRM, },
}
通过APP申请的READ_EXTERNAL_STORAGE权限找到对应组名sdcard_r,在android_ids结构中通过组名sdcard_r找到GID为AID_SDCARD_R,查看宏定义GID为1028,因此1028这个GID会作为补充GID赋予云音乐APP。
可稍作验证:
root@hammerhead:/ # grep "com.netease.cloudmusic" /data/system/packages.list
grep "com.netease.cloudmusic" /data/system/packages.list
com.netease.cloudmusic 10074 0 /data/data/com.netease.cloudmusic default 3002,3001,3003,1028,1015
云音乐的包名为com.netease.cloudmusic,UID为10074,补充GID为(3002,3001,3003,1028,1015),发现GID1028在列。
APP、系统服务等高层组件,通过包管理器进行权限控制,用不到上面的进程补充GID。下面以低层组件,内核层、原生守护进程为例,说明补充GID在权限控制中的使用。
1、内核层权限执行
Android系统对普通文件、设备节点文件和本地套接字的访问控制,和普通Linux系统一样。但对使用网络套接字,Android增加了特有的控制机制,那些创建网络套接字的进程需要属于inet组。
看一下Android内核中网络访问控制的实现(af_inet.c):
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
--snip--
if (!current_has_network())
return -EACCES;
--snip--
}
#ifdef CONFIG_ANDROID_PARANOID_NETWORK
#include
static inline int current_has_network(void)
{
return in_egroup_p(AID_INET) || capable(CAP_NET_RAW);
}
#else
static inline int current_has_network(void)
{
return 1;
}
#endif
那些不属于AID_INET组(GID 3003,名称inet)的进程,不会拥有CAP_NET_RAW的权能(允许使用RAW和PACKET套接字),因而会收到一个拒绝服务的错误(return -EACCES)。非Android的内核不会定义CONFIG_ANDROID_PARANOID_NETWORK宏,因此没有这个访问控制机制。
通过前面权限到GID映射可以知道,申请了INTERNET权限的APP,inet组GID会被自动映射到其补充GID,因此具有了使用网络套接字的权力。
可以看到内核的权限执行,使用了APP的进程补充GID,非包管理器中记录的APP权限信息。
2、原生守护进程的权限执行
原生守护进程一般使用UNIX域套接字进行进程间通信,而非Android首先的Binder。UNIX域套接字使用文件系统上的节点来表示,因此可以使用Linux标准的文件系统权限机制进行权限控制。
大多数套接字访问权限为只允许同用户/组的进程访问。而以不同UID和GID运行的客户端,是无法连接套接字的。系统守护进程的本地套接字由init.rc定义,该文件在init进程创建时使用。
查看用于设备卷管理的守护进程vold在init.rc中的定义:
service vold /system/bin/vold
class core
socket vold stream 0660 root mount
ioprio be 2
vold声明了一个同样名为vold,访问权限为0660的套接字。该套接字属于root,所属组为mount。vold守护进程需要以root运行,用以挂载/卸载卷设备,而mount组(AID_ MOUNT,GID 1009)的成员可以通过本地套接字向它发送指令,而无需以root用户运行,这也减小了root提权的攻击面。
除此之外,UNIX域套接字还提供了一种更细粒度的权限控制。
UNIX域套接字允许使用SCM_CREDENTIALS控制消息和SO_PEERCRED套接字选项,来传递和查询用户凭证。和有效UID、GID是Binder事务处理的一部分一样,与本地套接字相关联的凭证由内核填写,从而无法被用户进程伪造。
看一个例子,vold如何使用套接字客户端凭证进行细粒度访问控制:
//CommandListener.cpp
int CommandListener::CryptfsCmd::runCommand(SocketClient *cli,
int argc, char **argv) {
if ((cli->getUid() != 0) && (cli->getUid() != AID_SYSTEM)) {
cli->sendMsg(ResponseCode::CommandNoPermission,
"No permission to run cryptfs commands", false);
return 0;
}
--snip--
}
vold守护进程只允许以root或system运行的客户端发送加密的容器管理指令。这里UID通过SocketClient->getUid()方法获得,这个值是由getsocketopt(SO_PEERCRED)从客户端获得并初始化,这跟Binder中服务端实现的权限控制十分类似:
/* SocketClient.cpp */
void SocketClient::init(int socket, bool owned, bool useCmdNum) {
--snip--
struct ucred creds;
socklen_t szCreds = sizeof(creds);
memset(&creds, 0, szCreds);
int err = getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &creds, &szCreds);
if (err == 0) {
mPid = creds.pid;
mUid = creds.uid;
mGid = creds.gid;
}
}
同时,本地套接字的连接功能封装在了android.net.LocalSocket类中,可以被JAVA应用调用,允许高层系统服务和本地守护进程间进行通信,而无需使用JNI。例如,MountService框架类使用LocalSocket向vold进程发送指令。
三、APP 组件权限控制
基于包管理器的权限检查,貌似也没什么好说的。只简单总结一下。
1、Activity
如果传递到Context.startActivity()或startActivityForResult()方法的intent解析到一个声明权限的Activity时,就需要进行activity权限检查。如果调用者未申请权限,则抛出SecurityException异常。
2、Service
与Activity一样,只是权限检查时机不同,Service发生在Context.startService()、stopService()和bindService()方法调用时。
3、Broadcast(广播)
发送一个广播时,APP可以使用Context.sendBroadcast(Intent intent, String receiverPermission)方法,来要求接收者权限。接收者需要的权限,可以在manifest文件中指定,也可以在动态注册广播时指定。广播是异步的,因此权限执行发生在广播接收器接收到广播时。
与Activity和Service不同,广播的权限检查可以是双向的,也就是广播接收者同样可以要求广播发送者拥有某个特定权限,以便锁定发送者。
4、Content Provider
Content Provider的权限执行是最复杂的。可以保护整个组件或特定的导出URI,因此拥有更细的权限控制粒度。并且可以分别为读写指定不同的权限。
如果为读写分别指定了不同权限,那读权限控制谁可以调用目标Provider或URI的ContentResolver.query()方法,写权限控制谁可以对目标Provider或某个暴露的URI调用ContentResolver.insert()、update()和delete()方法。当某个方法被调用时,权限检查同步执行。
虽然权限检查的时机不同、粒度不同,但组件权限检查的原理都是前面讲的基于包管理器的权限管理机制。