第一章 Android的权限机制
Android是基于Linux的系统,其权限访问控制自然离不开Linux的权限访问控制,而在第一章当中,将分成两个部分来剖析Android的权限控制系统。
一. Linux权限机制
Linux的权限访问是由进程(访问者)和文件(被访问者)两部分组成的。其中相当一部分内容参考至APUE[1]。
1.1 Llinux文件权限
我们在Linux当中输入命令
$ls -l
我们可以看到这样类似如下的结果
drwxr-xr-x 2 root root 4096 11月 28 08:32 bin
drwxr-xr-x 3 root root 4096 12月 18 09:18 boot
drwxr-xr-x 2 root root 4096 3月 18 2012 cdrom
drwxr-xr-x 15 root root 4380 1月 4 19:28 dev
drwxr-xr-x 176 root root 12288 1月 4 19:01 etc
drwxr-xr-x 3 root root 4096 4月 16 2012 home
第一列使用的如drwxr-xr-x的10位字段,表示的是该文件的文件类型和其权限。下表描述了各个标志位的含义
9 | 6 - 8 | 3 - 5 | 0 - 2 |
文件类型 | 拥有者访问权限 | 所在用户组访问权限 | 其它用户访问权限 |
p 管道文件 d 目录文件 l 符号连接文件 - 普通文件 s socket文件 c 字符设备文件 b 块设备文件 |
分别为读写执行权限, -表示没有该位上的权限 读取权限: r 写入权限: w 执行权限: x s,S 表示设置了SUID位. s表示该位原标志为x, S表示该位原标志为- |
分别为读写执行权限, -表示没有该位上的权限 读取权限: r 写入权限: w 执行权限: x s,S 表示设置了GUID位. s表示该位原标志为x, S表示该位原标志为- |
分别为读写执行权限, -表示没有该位上的权限 读取权限: r 写入权限: w 执行权限: x s,S 表示设置了Sticky位. s表示该位原标志为x, S表示该位原标志为- |
表1 Linux文件权限标识符
特殊权限SGID标志位:普通文件设置了该标志位,则表示该进程的egid变成被运行的程序的所有者的gid。没有设置该位,则进程的egid为运行该程序的用户的gid。
特殊权限SUID标志位:普通文件设置了该标志位,则表示该进程的euid变成被运行的程序的所有者的uid。没有设置该位,则进程的euid为运行该程序的用户的uid。
特殊权限Sticky标志位:旧的UNIX系统定义该位为指示操作系统在程序退出后,保留程序的代码段到swap空间。而在linux系统当中,该位表示防删除位,意味着该位被设置之后,只有文件的拥有者和root用户才能删除该文件。[1][2]
SGID和SUID的存在意义在于,当一个非特权进程可以通过执行设置了SGID和SUID标志的程序,来获得特定权限。例如su,当它没有设置SGID和SUID标志位的时候,实际上它是不能创建一个具有root权限的shell进程的。
以上的文件权限标识符在Linux当中实际上是使用二进制来表示的,例如rwxrw-rw-,转成二进制形式就是111110110,但实际情况下,我们为了更方便阅读,我们使用的是八进制进行标识,也就是766。但是文件标识符当中除了基本读写执行之外,再算上特殊权限,实际上Linux权限访问控制使用的是12位二进制数字(3位特殊权限 + 9位基础权限)来表示访问权限。比如rwsrw-rw-,转化成八进制表示方式,就是4766。
1.2 linux进程权限
假设,我们在系统当中运行了一个程序,然后我们通过ps命令进行查询,得知该进程的pid为1025,之后我们在linux当中输入命令
$cat proc/1025/status
我们可以看到其中有一段
Name: live.androidpad
Uid: 10040 10040 10040 10040
Gid: 10040 10040 10040 10040
Groups: 1007 1015 3003
其中,Uid行有四列,它们分别为RUID,EUID,SUID,FSUID
RUID(实际用户id:Real User ID):进程的创建用户。
EUID(有效用户id:Effective User ID):进程的有效用户,用于权限访问控制。
SUID(保存设置用户id:Saved Set-User-ID):在程序执行(exec)之后作为EUID的副本,用于进程切换自己的EUID时使用,对用户来说实际意义不大。参考[1]
FSUID(文件系统用户id:File System User ID):Linux新引进的一类用户、组,用于文件访问控制。(推测,文件访问上FSUID优先于EUID)
Gid行有四列,它们分别为RGID,EGID,SGID,FSGID
RGID(实际用户id:Real User ID):进程的创建用户组
EGID(有效用户id:Effective User ID):进程的有效用户组,用于权限访问控制。
SGID(保存设置用户id:Saved Set-User-ID):在程序执行(exec)之后作为EGID的副本,用于进程切换自己的EGID时使用,对用户来说实际意义不大。参考[1]
FSGID(文件系统用户id:File System User ID):Linux新引进的一类用户、组,用于文件访问控制。(推测,文件访问上FSGID优先于EGID)
Groups行是组id,里面是一组使用空格分开的数字,这些数字就是是用户组的id,它同样用于权限访问控制。
对于FSGID和FSUID,这个东西是Linux中引进的,很多时候它的值是直接复制EGID和EUID的。而Unix系统当中,RUID\EUID\SUID、RGID\EGID\SGID和Groups作为标配,我们这里只讨论进程的这7个参数。正如我们使用命令输出的结果一样,除了Groups参数使用整形数组来表示之外,其余6个参数在Linux系统当中使用的都是整形来表示。而,这几个参数都会决定进程的权限等特性,而它们是基于什么规则来赋值的呢?
虽然实际上跟文件访问权限有关的仅仅是EUID、EGID和Groups,但是因为文章的受众很可能是只了解Android系统的开发者,所以我这里也多讲一些。Linux当中所有的进程创建都是通过fork函数创建的,当进程被fork之后,子进程会继承父进程的RUID\EUID和RGID\EGID,而SUID和SGID会在exec之后作为EUID和EGID的副本被赋值(关于fork和exec的更多讲解,请参考APUE[1]和[3])。而在进程创建之后,子进程可以通过setuid和setgid修改自身的RUID\EUID\SUID、RGID\EGID\SGID,但是这是有固定规则的。
以下是setuid的使用规则,setgid也与之类似:
1.若进程拥有超级权限,则setuid函数将RUID\EUID\SUID设置为uid。
2.若进程没有超级权限,而uid的值等于RUID或者SUID,则setuid将会把EUID设置为uid。而不会改变RUID或者SUID的值。
3.如果上述两个条件都不满足,则返回失败。
1.3 Linux的权限访问控制
这部分很简单,所有的系统调用最终都到内核当中,内核作为管理中枢,对所有的文件访问调用进行了核查。而APUE描述了内核对读写执行权限的测试算法:
1.若进程的EUID是0,则允许访问。
2.若进程的EUID等于所有者ID,那么:若所有者对应的访问权限位被设置,则允许访问,否则拒绝访问。
3.若进程的EGID或者附加组ID之一等于文件的组ID,那么:若组对应的访问权限位被设置,则允许访问,否则拒绝访问。
4.若其它用户对应的访问权限位被设置,则允许访问,否则拒绝访问。
二. Android权限机制
原本想对这部分内容进行详细解析的,但后来发现涉及的内容包含了PKMS,AMS,应用程序安装,应用程序启动等内容。假若我来描写这些内容,第一,篇幅太多,第二,自己的描述能力有限容易误导别人。所以我就不深入说了,有兴趣的朋友可以参考[4][5][6]。
在这里我们只需要知道,Android的策略是这样的:
1.文件和设备访问,使用Linux的权限访问控制。部分权限声明之后,应用程序启动的时候,AMS会从PKMS那里获得该应用进程的uid,gid和组id信息,然后通过Zygote来创建一个指定id的进程。获得指定组id的进程,也会获得部分文件的访问权限,例如声明android.permission.WRITE_EXTERNAL_STORAGE来访问sdcard会被赋予sdcard_rw的组id。权限所对应的组id在frameworks/base/data/etc/platform.xml当中。
特别注意:第一章也描述了,内核检查id的顺序是EUID然后再到EGID和组ID,所以,当你声明android.permission.WRITE_EXTERNAL_STORAGE的同时,声明shareUserId为system,是没有读写sdcard权限的。
2.Android接口调用控制,首先是root用户和system用户拥有所有的接口调用权限,然后对于其它用户使用Context以下这几个函数来实现
Context.checkCallingOrSelfPermission(String);
Context.checkCallingOrSelfUriPermission(Uri,int);
Context.checkCallingPermission(Permission);
Context.checkCallingUriPermission(Uri,int);
Context.checkPermission(String,int,int);
Context.checkUriPermission(Uri,int,int,int);
Context.checkUriPermission(Uri,String,String,int,int,int);
Context.enforceCallingOrSelfPermission(String,String);
Context.enforceCallingOrSelfUriPermission(Uri,int,String);
Context.enforceCallingPermission(String,String);
Context.enforceCallingUriPermission(String,String);
Context.enforcePermission(String,int,int,String);
Context.enforceUriPermission(Uri,int,int,int,String);
Context.enforceUriPermission(Uri,String,String,int,int,int,String);
其中check开头的,只做检查。enforce开头的,不单检查,没有权限的还会抛出异常。
这几个函数最后会调用到PKMS的checkUidPermission,该函数通过对比应用权限信息来判断该应用是否获得权限。
3.Android权限等级划分为normal,dangerous,signature,signatureOrSystem,system,development,其中
signature需要签名才能赋予权限,
signatureOrSystem需要签名或者系统级应用(放置在/system/app目录下)才能赋予权限,
system系统级应用(放置在/system/app目录下)才能赋予权限,系统权限的描述在frameworks/base/core/res/AndroidManifest.xml当中。
这就解答了,为什么有时候声明一些权限没有起作用,例如android.permission.WRITE_MEDIA_STORAGE。
如果我们想知道某个权限怎么使用,有什么制约怎么办?
pm list permissions -f
来查看系统所有权限的描述
如果我们需要在系统中增加一个权限,怎么办?那我们照下列的步骤来做
1.确定你的权限属于文件访问控制,还是接口调用控制。
2.在frameworks/base/core/res/AndroidManifest.xml,中增加你的权限描述。
3.如果是文件访问控制,那就在frameworks/base/data/etc/platform.xml为你的权限依附指定的组id。
4.如果是接口调用控制,那就在你的接口调用里面,加入上述Context检查权限的函数。
(这段内容确实不大好写,酝酿了好久,再酝酿就胎死腹中了,再度吐槽一下自己的描述能力。:-)第二章内容会讲述一下Android root的原理。)
参考资料
[1]《Advanced Programming in the UNIX Environment》, W.Richard Stevens.
[2] Sticky标志位, http://en.wikipedia.org/wiki/Sticky_bit
[3] Linux下Fork与Exec使用, http://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html
[4]《Android 内核剖析》,柯元旦.
[5]《深入理解Android》 ,邓平凡.
[6] Android权限官方文档 ,http://developer.android.com/intl/zh-CN/guide/topics/security/permissions.html.