Android 安全机制概述 Permission【转】
1 Android 安全机制概述
Android是一个权限分离的系统。这是利用Linux已有的权限管理机制,通过为每一个Application分配不同的uid和gid,从而使得不同的Application之间的私有数据和访问(native以及java层通过这种sandbox机制,都可以)达到隔离的目的。与此同时,Android还在此基础上进行扩展,提供了permission机制,它主要是用来对Application可以执行的某些具体操作进行权限细分和访问控制,同时提供了per-URIpermission机制,用来提供对某些特定的数据块进行ad-hoc方式的访问。
1.1uid、gid、gids
Android的权限分离的基础是建立在Linux已有的uid、gid、gids基础上的。
UID。Android在安装一个应用程序,就会为它分配一个uid(参考PackageManagerService中的newUserLP实现)。其中普通Android应用程序的uid是从10000开始分配(参见Process.FIRST_APPLICATION_UID),10000以下是系统进程的uid。
GID。对于普通应用程序来说,gid等于uid。由于每个应用程序的uid和gid都不相同,因此不管是native层还是java层都能够达到保护私有数据的作用。
GIDS。gids是由框架在Application安装过程中生成,与Application申请的具体权限相关。如果Application申请的相应的permission被granted,而且中有对应的gids,那么这个Application的gids中将包含这个gids。
uidgidgids的详细设置过程:
请参考ActivityManagerService中的startProcessLocked。在通过zygote来启动一个process时,直接将uid传给给了gid。再通过zygote来fork出新的进程(zygote.java中的forkAndSpecialize),最终在native层(dalvik_system_zygote.c)中的forkAndSpecializeCommon中通过linux系统调用来进行gid和uid和gids的设置。
1.2permission
一个权限主要包含三个方面的信息:权限的名称;属于的权限组;保护级别。一个权限组是指把权限按照功能分成的不同的集合。每一个权限组包含若干具体权限,例如在COST_MONEY组中包含android.permission.SEND_SMS,android.permission.CALL_PHONE等和费用相关的权限。
每个权限通过protectionLevel来标识保护级别:normal,dangerous,signature,signatureorsystem。不同的保护级别代表了程序要使用此权限时的认证方式。normal的权限只要申请了就可以使用;dangerous的权限在安装时需要用户确认才可以使用;signature和signatureorsystem的权限需要使用者的app和系统使用同一个数字证书。
Package的权限信息主要通过在AndroidManifest.xml中通过一些标签来指定。如<permission>标签,<permission-group>标签<permission-tree>等标签。如果package需要申请使用某个权限,那么需要使用<use-permission>标签来指定。
2Androidpermission管理机制
2.1Frameworkpermission机制
2.1.1安装入口
permission的初始化,是指permission的向系统申请,系统进行检测并授权,并建立相应的数据结构。绝大多数的情况下permission都是从一个package中扫描所得,而这发生在package安装和升级的时候。一般有如下几种安装入口:
npackageInstaller,package被下载安装时会触发使用。packageInstaller会通过AppSecurityPermissions来检查dangerous的权限,并对用户给出提示。
npm命令。
nadbinstall。最终还是调用pminstall来安装apk包。
n拷贝即安装。PackageManagerService中使用AppDirObserver对/data/app/进行监视,如果有拷贝即触发安装。
这些安装方式最终都会通过调用PackageManagerService中的函数来完成程序的安装。
2.1.2permission创建
第一步,从AndroidManifest.xml中提取permission信息。主要提取如下信息:
²shareduid
指定与其它package共享同一个uid。
²permission
提取permissions标签指定属性。它使用permissionInfo来描述一个权限的基本信息。需要指定protectedLevel信息,并指定所属group信息。它将被添加到这个package的permissions这个list结构中。
²permission-tree
提取permissions-tree标签属性。permissions-tree也通过permissionInfo来描述,并被添加到package的permissions这个list结构中。permission-tree只是一个名字空间,用来向其中动态添加一些所谓Dynamic的permission,这些permission可以动态修改。这些permission名称要以permission-tree的名称开头。它本身不是一种权限,没有protectedLevel和所属group。只是保存了所属的packge和权限名(带有package前缀的)。
²permission-group
定义permission组信息,用PermissionGroup表示。本身不代表一个权限,会添加进入package的permissionGroups这个list中。
²uses-permission
定义了package需要申请的权限名。将权限名添加到package的requestedPermissions这个list中。
²adopt-permissions
将该标签指定的name存入package的mAdoptPermissions这个list中。Name指定了这个package需要从name指定的package进行权限领养。在systempackage进行升级时使用。
第二步。获取Package中的证书,验证,并将签名信息保存在Package结构中。
1.如果该package来自systemimg(系统app),那么只需要从该Package的AndroidManifest.xml中获取签名信息,而无需验证其完整性。但是如果这个package与其它package共享一个uid,那么这个共享uid对应的sharedUser中保存的签名与之不一致,那么签名验证失败。
2.如果是普通的package,那么需要提取证书和签名信息,并对文件的完成性进行验证。
第三步。如果是普通的package,那么清除package的mAdoptPermissions字段信息(系统package升级才使用)。
第四步。如果在AndroidManifest.xml中指定了shareduser,那么先查看全局list中(mSharedUsers)是否该uid对应的SharedUserSetting数据结构,若没有则新分配一个uid,创建SharedUserSetting并保存到全局全局list(mSharedUsers)中。
mUserIds保存了系统中已经分配的uid对应的SharedUserSetting结构。每次分配时总是从第一个开始轮询,找到第一个空闲的位置i,然后加上FIRST_APPLICATION_UID即可。
第五步。创建PackageSettings数据结构。并将PackageSettings与SharedUserSetting进行绑定。其中PackageSettings保存了SharedUserSetting结构;而SharedUserSetting中会使用PackageSettings中的签名信息填充自己内部的签名信息,并将PackageSettings添加到一个队列中,表示PackageSettings为其中的共享者之一。
在创建时,首先会以packageName去全局数据结构mPackages中查询是否已经有对应的PackageSettings数据结构存在。如果已经存在PackageSettings数据结构(比如这个package已经被uninstall,但是还没有删除数据,此时package结构已经被释放)。那么比较该package中的签名信息(从AndroidManifest中扫描得到)与PackageSettings中的签名信息是否匹配。如果不匹配但是为systempackage,那么信任此package,并将package中的签名信息更新到已有的PackageSettings中去,同时如果这个package与其它package共享了uid,而且shareduid中保存的签名信息与当前package不符,那么签名也验证失败。
第六步。如果mAdoptPermissions字段不为空,那么处理permission的领养(从指定的package对应的PackageSettings中,将权限的拥有者修改为当前package,一般在systemapp升级的时候才发生,在此之前需要验证当被领养的package已经被卸载,即检查package数据结构是否存在)。
第七步。添加自定义权限。将package中定义的permissionGroup添加到全局的列表mPermissionGroups中去;将package中定义的permissions添加到全局的列表中去(如果是permission-tree类型,那么添加到mSettings.mPermissionTrees,如果是一般的permission添加到mSettings.mPermissions中)。
第八步。清除不一致的permission信息。
1.清除不一致的permission-tree信息。如果该permission-tree的packageSettings字段为空,说明还未对该package进行过解析(若代码执行到此处时packageSettings肯定已经被创建过),将其remove掉。如果packageSettings不为空,但是对应的package数据结构为空(说明该package已经被卸载,但数据还有保留),或者package数据结构中根本不含有这个permission-tree,那么将这个permission-tree清除。
2.清除不一致的permission信息。如果packageSettings或者package结构为空(未解析该package或者被卸载,但数据有保留),或者package中根本没有定义该permission,那么将该permission清除。
第九步。对每一个package进行轮询,并进行permission授权。
1.对申请的权限进行检查,并更新grantedPermissions列表
2.如果其没有设置shareduserid,那么将其gids初始化为mGlobalGids,它从permission.xml中读取。
3.遍历所有申请的权限,进行如下检查
1)如果是该权限是normal或者dangerous的。通过检查。
2)如果权限需要签名验证。如果签名验证通过。还需要进行如下检查
*如果程序升级,而且是systempackage。那么是否授予该权限要看原来的package是否被授予了该权限。如果被授予了,那么通过检查,否则不通过。
*如果是新安装的。那么检查通过。
4.如果3中检查通过,那么将这个permission添加到package的grantedPermissions列表中,表示这个permission申请成功(granted)。申请成功的同时会将这个申请到的permission的gids添加到这个package的gids中去。
5.将permissionsFixed字段标准为ture,表示这个packge的permission进行过修正。后续将禁止对非system的app的权限进行再次修正。
2.1.3Dynamicpermission的管理
PackageManagerService提供了addPermission/removePermission接口用来动态添加和删除一些权限。但是这些权限必须是所谓的动态权限(BasePermission.TYPE_DYNAMIC)。
一个Package如果要添加Dynamicpermissions,首先必须要在manifest中申明<permission-tree>标签,它实际上是一个权限的名字空间(例如,“com.foo.far”这个权限就是permission-tree“com.foo”的成员),本身不是一个权限。一个Package只能为自己的permission-tree或者拥有相同的uid的package添加或者删除权限。
Package不能够通过这种接口去修改在manifest中静态申请的权限,否则抛出异常。
首先查找这个permission在全局permission列表mSettings.mPermissions中是否存在。如果存在,而且类型为BasePermission.TYPE_DYNAMIC那么根据传入的权限信息修改全局表中的权限信息,并触发permissions.xml的持久化。
如果在全局的permission列表mSettings.mPermissions中没有找到,先找到这个permission所在permissionTree,然后添加到全局permission列表mSettings.mPermissions中去,并触发permissions.xml的持久化。
2.1.4Uripermission的管理
下面两个接口主要用于Uripermission的管理(其实现在ActivityManagerService中)。
//为指定的uid和targetPkg添加对某个contentUri的读或者写权限。
publicvoidgrantUriPermission(IApplicationThreadcaller,StringtargetPkg,Uriuri,intmode)throwsRemoteException;
//清除所有通过grantUriPermission对某个Uri授予的权限。
publicvoidrevokeUriPermission(IApplicationThreadcaller,Uriuri,intmode)throwsRemoteException;
grantUriPermission主要的实现过程分析。
grantUriPermission分析:
1.验证caller的ProcessRecord和targetPkg不为空。否则检测不通过。
2.验证所请求的mode为Intent.FLAG_GRANT_READ_URI_PERMISSION或者为Intent.FLAG_GRANT_WRITE_URI_PERMISSION,否则不通过。
3.确保参数Uri是一个contentUri。否则,则检测不通过。
4.通过Uri得到目标ContentProvider,如果不存在,则检测不通过。
5.从PackageManagerService中获得targetPkg对应的uid。
6.检查targetuid所对应的package是否真正需要这个权限?
先判断要申请的是读还是写权限,然后查看对应的ContentProvider中对应的readPermissionwritePermission字段是否保存了权限名称。如果该字段不为空,则以targetuid和该权限名去PackageManagerService中去查找该uid是否被granted了该权限。如果已经获得了该权限,那么无需再去为这个Activity去申请这个Uri权限了,返回。否者继续执行如下操作。
7.检查这个ContentProvider的grantUriPermissions开关变量,是否允许对其它package进行权限的grant操作。如果禁止,那么抛出异常。
8.检查这个ContentProvider是否设置了Uri的过滤类型uriPermissionPatterns,如果设置了过滤类型,则将需要申请权限的Uri与之匹配。匹配不同过,则抛出异常。
9.检查调用者自己是否有权限访问这个Uri。如果没有,抛出异常。
10.从mGrantedUriPermissions中取得targetuid对应的HashMap<Uri,UriPermission>数据结构。用targetuid和Uri生成UriPermission并保存在mGrantedUriPermissions中。
revokeUriPermission实现分析。
找到该Uri对应的ContentProvider,然后删除mGrantedUriPermissions中与Uri对应的所有权限。
2.2permission的动态检查
这里的动态检查是指是package在程序运行过程中进行某些操作或者数据访问时才进行的check,与之对应的是应用程序安装或者升级时PackageManagerService通过扫描包中的静态权限信息相对应。
系统与权限检查相关的机制的实现主要集中在PackageManagerService和ActivityManagerService中。ActivityManagerService主要负责的是底层的uid层次的身份检查;PackageManagerService则维护了uid到自己拥有的和被授予的权限的一张表。在通过ActivityManagerService的身份检查后,PackageManagerService根据请求者的uid来查看这张表,判断其是否具有相应的权限。
除此之外,per-URIpermission机制的实现也需要一张表,它维护在ActivityManagerService中,它建立了从contentURI到被授权访问这个URI的component之间的映射。但是它也需要借助PackageManagerService的机制来辅助实现。
2.2.1framework提供的接口
Androidframework中提供了一些接口用来对外来的访问(包括自己)进行权限检查。这些接口主要通过ContextWrapper提供,具体实现在ContextImpl中。如果package接受到外来访问者的操作请求,那么可以调用这些接口进行权限检查。一般情况下可以把这些接口的检查接口分为两种,一种是返回错误,另一种是抛出异常。
主要包含如下几组:
npermission和uid检查API
下面这一组接口主要用来检查某个调用(或者是其它package或者是自己)是否拥有访问某个permission的权限。参数中pid和uid可以指定,如果没有指定,那么framework会通过Binder来获取调用者的uid和pid信息,加以填充。返回值为PackageManager.PERMISSION_GRANTED或者PackageManager.PERMISSION_DENIED。
publicintcheckPermission(Stringpermission,intpid,intuid)//检查某个uid和pid是否有permission权限
publicintcheckCallingPermission(Stringpermission)//检查调用者是否有permission权限,如果调用者是自己那么返回PackageManager.PERMISSION_DENIED
publicintcheckCallingOrSelfPermission(Stringpermission)//检查自己或者其它调用者是否有permission权限
下面这一组和上面类似,如果遇到检查不通过时,会抛出异常,打印消息。
publicvoidenforcePermission(Stringpermission,intpid,intuid,Stringmessage)
publicvoidenforceCallingPermission(Stringpermission,Stringmessage)
publicvoidenforceCallingOrSelfPermission(Stringpermission,Stringmessage)
nper-URI检查API
为某个package添加访问contentUri的读或者写权限。
publicvoidgrantUriPermission(StringtoPackage,Uriuri,intmodeFlags)
publicvoidrevokeUriPermission(Uriuri,intmodeFlags)
检查某个pid和uid的package是否拥有uri的读写权限,返回值表示是否被granted。
publicintcheckUriPermission(Uriuri,intpid,intuid,intmodeFlags)
publicintcheckCallingUriPermission(Uriuri,intmodeFlags)
publicintcheckCallingOrSelfUriPermission(Uriuri,intmodeFlags)
publicintcheckUriPermission(Uriuri,StringreadPermission,StringwritePermission,intpid,intuid,intmodeFlags)
检查某个pid和uid的package是否拥有uri的读写权限,如果失败则抛出异常,打印消息。
publicvoidenforceUriPermission(Uriuri,intpid,intuid,intmodeFlags,Stringmessage)
publicvoidenforceCallingUriPermission(Uriuri,intmodeFlags,Stringmessage)
publicvoidenforceCallingOrSelfUriPermission(Uriuri,intmodeFlags,Stringmessage)
publicvoidenforceUriPermission(Uriuri,StringreadPermission,StringwritePermission,intpid,intuid,intmodeFlags,Stringmessage)
2.2.2实现分析
ContextImpl.java中提供的API,其实都是由ActivityManagerService中的如下几个接口进行的封装。
publicintcheckPermission(Stringpermission,intpid,intuid)throwsRemoteException;//主要用于一般的permission检查
publicintcheckUriPermission(Uriuri,intpid,intuid,intmode)throwsRemoteException;//主要用于ContentUri的permission检查
ncheckPermission的实现分析
1.如果传入的permission名称为null,那么返回PackageManager.PERMISSION_DENIED。
2.判断调用者uid是否符合要求。
1)如果uid为0,说明是root权限的进程,对权限不作控制。
2)如果uid为systemserver进程的uid,说明是systemserver,对权限不作控制。
3)如果是ActivityManager进程本身,对权限不作控制。
4)如果调用者uid与参数传入的requid不一致,那么返回PackageManager.PERMISSION_DENIED。
3.如果通过2的检查后,再调用PackageManagerService.checkUidPermission,判断这个uid是否拥有相应的权限,分析如下。
1)首先它通过调用getUserIdLP,去PackageManagerService.Setting.mUserIds数组中,根据uid查找uid(也就是package)的权限列表。一旦找到,就表示有相应的权限。
2)如果没有找到,那么再去PackageManagerService.mSystemPermissions中找。这些信息是启动时,从/system/etc/permissions/platform.xml中读取的。这里记录了一些系统级的应用的uid对应的permission。
3)返回结果。
n同样checkUriPermission的实现主要在ActivityManagerService中,分析如下:
1.如果uid为0,说明是root用户,那么不控制权限。
2.否则,在ActivityManagerService维护的mGrantedUriPermissions这个表中查找这个uid是否含有这个权限,如果有再检查其请求的是读还是写权限。
3Android签名机制
关于签名机制,其实分两个阶段。
包扫描阶段需要进行完整性和证书的验证。普通package的签名和证书是必须要先经过验证的。具体做法是对manifest下面的几个文件进行完整性检查。完整性检查包括这个jar包中的所有文件。如果是系统package的话,只需要使用AndroidMenifest.xml这个文件去提取签名和验证信息就可以了。
在权限创建阶段。如果该package来自systemimg(系统app),那么trustit,而且使用新的签名信息去替换就的信息。前提是如果这个package与其它package共享一个uid,那么这个共享uid对应的sharedUser中保存的签名与之不一致,那么签名验证失败。有些时候系卸载一个app,但是不删除数据,那么其PackageSettings信息会保留,其中会保存签名信息。这样再安装是就会出现不一致。
3.1AndroidPackage签名原理
android中系统和app都是需要签名的。可以自己通过development/tools/make_key来生成公钥和私钥。
android源代码中提供了工具./out/host/linux-x86/framework/signapk.jar来进行手动签名。签名的主要作用在于限制对于程序的修改仅限于同一来源。系统中主要有两个地方会检查。如果是程序升级的安装,则要检查新旧程序的签名证书是否一致,如果不一致则会安装失败;对于申请权限的protectedlevel为signature或者signatureorsystem的,会检查权限申请者和权限声明者的证书是否是一致的。签名相关文件可以从apk包中的META-INF目录下找到。
signapk.jar的源代码在build/tools/signapk,签名主要有以下几步:
l将除去CERT.RSA,CERT.SF,MANIFEST.MF的所有文件生成SHA1签名
首先将除了CERT.RSA,CERT.SF,MANIFEST.MF之外的所有非目录文件分别用SHA-1计算摘要信息,然后使用base64进行编码,存入MANIFEST.MF中。如果MANIFEST.MF不存在,则需要创建。存放格式是entryname以及对应的摘要
l根据之前计算的SHA1摘要信息,以及私钥生成一系列的signature并写入CERT.SF
对整个MANIFEST.MF进行SHA1计算,并将摘要信息存入CERT.SF中。然后对之前计算的所有摘要信息使用SHA1再次计算数字签名,并写入CERT.SF中。
l把公钥和签名信息写入CERT.RST
把之前整个的签名输出文件使用私有密钥计算签名。同时将签名结果,以及之前声称的公钥信息写入CERT.RSA中保存。
3.2Package的签名验证
安装时对一个package的签名验证的主要逻辑在JarVerifier.java文件的verifyCertificate函数中实现。其主要的思路是通过提取cert.rsa中的证书和签名信息,获取签名算法等信息,然后按照之前对apk签名的方法进行计算,比较得到的签名和摘要信息与apk中保存的匹配。
第一步。提取证书信息,并对cert.sf进行完整性验证。
1.先找到是否有DSA和RSA文件,如果找到则对其进行decode,然后读取其中的所有的证书列表(这些证书会被保存在Package信息中,供后续使用)。
2.读取这个文件中的签名数据信息块列表,只取第一个签名数据块。读取其中的发布者和证书序列号。
3.根据证书序列号,去匹配之前得到的所有证书,找到与之匹配的证书。
4.从之前得到的签名数据块中读取签名算法和编码方式等信息
5.读取cert.sf文件,并计算整个的签名,与数据块中的签名(编码格式的)进行比较,如果相同则完整性校验成功。
第二步。使用cert.sf中的摘要信息,验证MANIFEST.MF的完整性。
在cert.sf中提取SHA1-Digest-Manifest或者SHA1-Digest开头的签名数据块(-Digest-Manifest这个是整个MANIFEST.MF的摘要信息,其它的是jar包中其它文件的摘要信息),并逐个对这些数据块进行验证。验证的方法是,现将cert.sf看做是很多的entries,每个entries包含了一些基本信息,如这个entry中使用的摘要算法(SHA1等),对jar包中的哪个文件计算了摘要,摘要结果是什么。处理时先找到每个摘要数据开中的文件信息,然后从jar包中读取,然后使用-Digest之前的摘要算法进行计算,如果计算结果与摘要数据块中保存的信息的相匹配,那么就完成验证。