AOSP-Android安全机制SEAndroid/SELinux
Android 平台利用基于用户的 Linux 保护机制识别和隔离应用资源,作为 Android 安全模型的一部分,Android 使用安全增强型 Linux (SELinux) 对所有进程强制执行强制访问控制 (MAC),甚至包括以 Root/超级用户权限运行的进程(Linux 功能)。
Android权限控制流程
Android中自主访问控制是通过Linux UID/GID实现,而强制访问控制则是使用的SEAndroid!
在Android中SEAndroid安全机制(MAC)与传统的Linux UID/GID安全机制(DAC)是并存关系的,也就是说,它们同时用来约束进程的权限。当一个进程访问一个文件的时候,首先要通过基于UID/GID的DAC安全检查,接着才有资格进入到基于SEAndroid的MAC安全检查。只要其中的一个检查不通过,那么进程访问文件的请求就会被拒绝。
DAC自主访问控制
自主访问控制,正式的英文名称为Discretionary Access Control,简称为DAC。
比如通过 ls -l /system ,可以查看到该目录下存在一个manifest.xml文件,其输出为:
1 | -rwxr-x--- 1 root root 2544 2022-09-29 17:02 manifest.xml |
表示 manifest.xml是root用户组的root用户拥有,对于root用户来说,是rwx(可读可写可执行);而对于root用户
组其他用户来说,是可读可执行;对于其他用户则没有任何权限;也就是750权限。
那我们的程序能够对该文件进行写操作呢?在设备中运行程序执行:
1 | File file = new File("/system/manifest.xml"); |
会输出:false false false,因为当前程序UID不可能是root(可以通过 data/system/packages.list 文件查看)。
我们知道,Android是一个基于Linux内核的系统,但是它不像传统的Linux系统,需要用户登录之后才能使用。然
而,Android系统又像传统的Linux系统一样有用户的概念。只不过这些用户不需要登录,也可以使用Android系统。
这是因为Android系统将每一个安装在系统的APK都映射为一个不同的Linux用户。也就是说,每一个APK都有一个对应的UID和GID。这些UID和GID是在APK安装的时候由系统安装服务PMS分配的:
在系统源码/base./services/core/java/com/android/server/pm/PackageManagerService.java:
中如下:
1 | private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, |
在系统源码/base./services/core/java/com/android/server/pm/Settings.java:
中如下:
1 | void addUserToSettingLPw(PackageSetting p) throws PackageManagerException { |
在完成安装并运行程序后,可以通过 ps -A | grep PACKAGENAME 查看程序uid:
1 | angler:/system # ps -A | grep com.enjoy |
得到当前程序进程ID为7124,然后通过 cat /proc/7124/status 查看:
1 | cat /proc/7124/status |
可以看到当前程序UID为10072,通过这种方式,就可以保证每一个APK进程都以不同的身份来运行,从而保证了相
互之间不会受到干扰。这就是所谓的沙箱了,这完全是建立在Linux的UID和GID基础上的。
root的UID/GID可以通过下面方式查看:
在系统源码/system/core/include/private/android_filesystem_config.h
下
1 | ...... |
可以看到,普通APP的UID从10000开始分配,最大到19999,而root用户的id为0。很显然程序并不具备对
manifest.xml文件的读写权限。
这种基于Linux UID/GID的安全机制,我们称之为自主访问控制,正式的英文名称为Discretionary Access Control,简称为DAC。
应用权限与DAC的关系
如何才能让我们的进程能通过Linux UID/GID的拦截呢?如果是一个Android APP若让其具备网络权限,我们只需要在AndroidManifest.xml中配置:
1 | <uses-permission android:name="android.permission.INTERNET"/> |
APP的UID和GID是在安装时候就由PMS分配好了的。为什么这样一个配置就能够让程序通过Linux UID/GID的拦截?
这是因为PMS在安装APK时,从Manifest文件中把App信息和权限存到 /data/system/packages.xml和 /data/system/packages.list 文件中。以《百度作业帮》为例,打开packages.list会存在下面的记录:
1 | com.baidu.homework 10051 0 /data/user/0/com.baidu.homework default:targetSdkVersion=26 3002,3003,3001 |
其中3002,3003,3001代表的就是用户组,通过 /system/core/include/private/android_filesystem_config.h 查看可知
1 |
3001与3002代表了具备蓝牙相关权限的用户组,而3003则表示具备网络权限的用户组。PMS会将APK加入到相应的某个Linux用户组去,这样APK才能够具备对应的权限。
运行作业帮后执行:
1 | #执行 |
可以看到在Groups中存在3003,因此作业帮才具备网络访问的权限!Android应用权限与Linux UID/GID权限就是因此而关联起来的。
DAC的问题
在理想情况下,DAC机制是没有问题的。然而,现实很骨感。比如我们将某个系统文件的权限改为777(可读可写可执行),那是不是意味着我们的程序就能随意修改系统的配置了呢?我们还是以/system/manifest.xml为例:
我们通过以下命令将/system挂载为可读可写,并修改manifest文件读写权限:
1 | adb root |
修改完成后,如果只有DAC机制那么任何用户都能具备对该文件的读写以及执行权限,造成严重的安全问题。然而Android中我们会发现哪怕修改了777权限,app仍然无法对该文件进行写操作,这是因为Android中还是使用了一种更为强有力的安全机制来保证系统的安全,这种机制就是MAC!
Android MAC强制访问控制
完成上文的对manifest文件权限的修改后,再执行我们的程序运行:
1 | File file = new File("/system/manifest.xml"); |
此时输出:true false true。可以看到我们以及可以在程序中对该文件进行读取,然而还是无法写这个文件。
这就是MAC强制访问控制的作用,在MAC机制中,用户、进程或者文件的权限是由管理策略决定的,而不是由它们自主决定的。例如,我们可以设定这样的一个管理策略,不允许用户A将它创建的文件F授予用户B访问。这样无论用户A如何修改文件F的权限位,用户B都是无法访问文件F的。这种安全访问模型可以强有力地保护系统的安全。
SELinux/SEAndroid
Android中使用的MAC机制就是SEAndroid。SELinux(Security-Enhanced Linux) 是美国国家安全局(NSA)在Linux
社区的帮助下设计的一个Linux历史上最杰出的安全系统,是一种MAC机制(Mandatory Access Control,强制访问控制)。在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。
由于Android系统有着独特的用户空间运行时,因此SELinux不能完全适用于Android系统。为此,NSA针对Android系统,在SELinux基础上开发了SEAndroid。
可参考官方文档:
https://source.android.google.cn/docs/security/features/selinux?hl=zh_cn
SEAndroid权限配置实战
SEAndroid安全机制又称为是基于TE(Type Enforcement)策略的安全机制。在/system/sepolicy目录中,所有
以.te为后缀的文件均为策略配置文件。我们以fdbus的name-server配置为例进行示例操作。
安全上下文
SEAndroid安全机制中的安全策略是在安全上下文的基础上进行描述的,也就是说,它通过主体和客体的安全上下
文,定义主体是否有权限访问客体。主体通常就是进程,而客体就是指进程所要访问的资源,例如文件、系统属性
等。
由于我们的服务程序可执行文件为/system/bin/name-server
,首先我们需要在/system/sepolicy/private/file_contexts
中声明该执行文件的SEAndroid系统文件的安全上下文:
1 | /system/bin/name-server u:object_r:name-server_exec:s0 |
安全上下文实际上就是一个附加在对象上的标签(Tag)。这个标签实际上就是一个字符串,它由四部分内容组成,分别是:
SELinux用户:SELinux角色:类型:安全级别
每一个部分都通过一个冒号来分隔,格式为”user:role:type:sensitivity”。
在安全上下文中,只有类型(Type)才是最重要的,SELinux用户、SELinux角色和安全级别都几乎可以忽略不计的。正因为如此,SEAndroid安全机制又称为是基于TE(Type Enforcement)策略的安全机制。
用户与角色
在/system/sepolicy/private/users
中声明了SELinux用户u,它可用的SELinux角色为r,它的默认安全级别为s0,可
用的安全级别范围为s0 - mls_systemhigh:
1 | user u roles { r } level s0 range s0 - mls_systemhigh; |
mls_systemhigh为系统定义的最高安全级别。
在/system/sepolicy/public/roles
中声明了SELinux角色r与类型domain关联:
1 | role r types domain; |
在SEAndroid中,只定义了唯一一个用户u,两个角色r(适用于主题,如进程)与object_r(适用于对象,如文件),
这意味着只有u、r/object_r和domain可以组合在一起形成一个合法的安全上下文,那么ps -Z查看进程应为u:r:domain:s0
,ls -Z查看文件则为: u:object_r:domain:s0
,而其它形式的安全上下文定义均是非法的。
但是以init进程为例,执行 adb shell ps -Z | grep zygote
(Windows为: adb shell ps -Z | findStr zygote
),可以看到输出为:
1 | u:r:zygote:s0 root 646 1 1577904 69500 poll_schedule_timeout eb7e743c S zygote |
安全上下文 u:r:zygote:s0 ,按照上面的分析,这不是应该是一个不合法的上下文吗?原因是在 /system/sepolicy/public/zygote.te
中通过type声明了类型zygote并且将domain设置为类型zygote的属
性:
1 | type zygote, domain; |
因此它就可以像domain一样,可以和SELinux用户u和SELinux角色组合在一起形成合法的安全上下文。
类型
在SEAndroid中,每一个用来描述文件安全上下文的类型都将file_type设置为其属性,每一个用于进程安全上下文的类型都将domain设置为其属性。
安全策略
前面提到,SEAndroid安全机制主要是使用对象安全上下文中的类型来定义安全策略,这种安全策略就称Type
Enforcement,简称TE,.te文件即为安全策略配置文件。
修改 system/sepolicy/prebuilts/api/31.0/private
与 system/sepolicy/private/file_contexts
目录下同
样的:file_contexts、netd.te与untrusted_app_all.te文件,并在两个目录下都创建name-server.te文件。
按照google官方的介绍:
system/sepolicy/public:公共策略配置,将此目录视为相应平台的已导出政策 API,包括供应商特定
策略。system/sepolicy/private:系统正常运行所必需(但供应商映像政策应该不知道)的策略。
对于权限的配置不建议直接修改以上目录,应该在 /device/manufacturer/device-name/sepolicy 目录进
行自己设备的专用策略配置。
如Pixel5手机搭载AAOS,则应该在: /device/google_car/redfin_car/sepolicy 目录下配置。同时修改或
添加政策文件和上下文的描述文件后,需要修改 /device/manufacturer/device-name/BoardConfig.mk 以
引用 sepolicy 子目录和每个新的政策文件。
1
2
3
4
5
6 BOARD_SEPOLICY_DIRS += \
<root>/device/manufacturer/device-name/sepolicy
BOARD_SEPOLICY_UNION += \
genfs_contexts \
file_contexts \
sepolicy.te
由于我们的进程名为name-server,因此在 /system/sepolicy/private 目录下创建一个name-server.te作为该进
程的策略文件。文件内容如下:
1 | 声明name-server类型,并将domain属性关联到该类型 (进程) |
Allow规则
allow表示开放权限,当某个进程执行,如果该进程不具备对应的权限,则可以在logcat 或者在执行 adb root 后执
行 adb shell dmesg 查看,可以以执行 adb shell dmesg > xx.txt 将内核日志都输出到xx.txt中查看。
官方资料:https://source.android.google.cn/docs/security/selinux/concepts?hl=zh_cn
1 | avc: denied { bind } for pid=417 comm="name-server" scontext=u:r:name-server:s0 |
说明 | 案例 |
---|---|
缺少什么权限 | { bind } |
谁缺少权限 | scontext=u:r:name-server:s0 |
对谁缺少权限 | tcontext=u:r:name-server:s0 |
什么类型的权限 | tclass=tcp_socket |
此时就需要在TE文件中声明:
1 | allow [谁缺少权限] [对谁缺少权限]:[什么类型的权限] [缺少什么权限] |
所以通过日志,就能完成对SEAndroid权限的赋予!
若验证是否为SEAndroid权限导致的程序无法正常运行可以通过关闭临时SEAndroid验证:
1
2
3
4
5
6
7 adb root
关闭seandroid
adb shell setenforce 0
开启seandroid
adb shell setenforce 0
查看seandroid
adb shell getenforce
audit2allow
https://source.android.google.cn/docs/security/selinux/validate?hl=zh_cn#using_audit2allow
audit2allow 工具可以获取 dmesg 拒绝事件并将其转换成相应的 SELinux 政策声明。因此,该工具有助于大幅加快 SELinux 开发速度。
如需使用该工具,请运行以下命令:(需要切换到Python2)
1 | 保证当前终端先执行了source与lunch指令 |
如上图,表示需要在name-server.te中增加:
1 | allow name-server self:netlink_route_socket read; |
宏函数
在system/sepolicy/public/te_macros中定义了很多宏函数,其中init_daemon_domain表示声明 name-server
是从 init 衍生而来的,并且可以与其通信。
另外还有其他宏如:net_domain,其定义为:
1 | #################################### |
如果调用该宏:net_domain(name-server)表示将name-server赋予netdomain类型。而netdomain类型可以在
system/sepolicy/public/net.te 中看到,存在如下规则:
1 | ...... |
那么使用该宏则表示,将name-server赋予tcp_socket对应的create_stream_socket_perms权限。整体而言
net_domain函数表示允许 使用 net 域中的常用网络功能,如读取和写入 TCP 数据包、通过套接字进行通信,以及
执行 DNS 请求等。也就是说,可以通过对应的宏函数调用完成对某些类型权限的统一allow。