深入浅出理解Android系统中的SeLinux

前言

早年期间,Linux基于用户身份/用户组的DAC(Discretionary Access Control作为访问控制策略: 每个进程都有所属的UID, 每个文件都有所属的UID/GID以及文件模式(读写执行等), 一个进程是否可以访问某个文件就是基于UID/GID/文件模式来管理的.换句话说,只要某个资源序属于该用于或该用户组, 则该用户对该资源具有绝对控制权力, 这样一旦用户获得了root权限, 那么整个系统就成了肉鸡. 可见, DAC的安全控制策略比较粗放。

SELinux最初是由美国Utah大学与NSA(National Security Agency)的安全小组研究出来的安全框架FLASK演变而来, 后被合入到Linux 2.6版本.相较于DAC, SELinux采用的是更细粒度的MAC(Mandatory Access Control).对于DAC而言, 资源的权限是由每个用户自己控制的, 而MAC则将所有的权限收拢, 由一个统一的管理者(SELinux)统一来分配所有的资源权限, 如果访问者没有事先分配到某个资源的权限, 则不会允许访问.这样即使是root用户也要收到安全策略的约束. Android在4.3开始引入SELinux, 到了5.0版本之后, 则开始全面支持了.在SELinux机制下, Android中所有的对象(进程/文件/socket/property)都打上了标签(label), 进程访问对象时, SELinux根据事先配置好的安全策略(security policy)判断访问者是否有权限.

  • SeLinux机制:在SELinux机制下,每个进程都被打上了label(称为process label),所有的对象(进程/文件/目录/网络口/socket/property/设备等对象)都打上了标签(称为Object label)。

  • MAC:SeLinux通过编写规则来控制一个process label对一个Object label的访问,这个规则称之为策略(Policy)。SeLinux的这种安全机制称为Mandatory Access Control (MAC),是在内核层实现的。
    MAC策略

  • DAC:在标准的Linux上,也就是未实施SeLinux的Linux上,传统的访问控制是通过owner/group + permission flags(比如rwx)实现的,称之为Discretionary Access Control (DAC).

SeLinux和传统的DAC不是替代的关系,而是并行的关系。两者同时在起作用。之所以出现SeLinux,是因为传统DAC的安全机制过于粗粝,而Selinux提供了更为细致和安全的访问控制。简言之,传统DAC机制下,一旦你获得了root权限,将无所不能,但在SeLinux的机制下,即使获得了root权限,也仍然需要遵循已经设置好的访问策略,只有指定的进程才可以访问指定的文件。Android系统是同时支持DAC与SELinux的, 就是说, 在一个进程访问某个资源时,会按照如下规则进行权限控制:

  • 首先根据DAC规则, 检查进程权限, 是否具有对应资源的读写/执行权限, 如果没有则拒绝执行;
  • 如果DAC规则检查通过, 则执行SELinux安全规则的检查, 如果不通过,则拒绝访问。

初识SELinux

SELinux的首要原则是: 任何未被声明允许执行的都会被拒绝, 其有两种运行模式:

  • Permissive模式: 访问控制的策略不会强制执行, 但是会被日志记录下来
  • Enforcing模式: 访问控制策略会被强制执行并被记录下来

在Android下可以通过 adb getenforce来查看当前SELinux处于何种模式。
getenforce指令
也可以通过adb setenforce来设置SELinux的模式,USER版本默认是enforcing, USERDEBUG/ENG可以设置模式:

adb setenforce 0 // permissive模式
adb setenforce 1 // enforcing模式

在SELinux中, 主要有Subject/Object/Object Manager/Security Server等几个核心的组成部分(见下图):

  • Subject: 在SELinux中, Subject是一个进程, 每个Subject都有与之关联的一个安全上下文(security context); Subject负责发起访问某个对象的请求,比如读文件/建立socket链接
  • Object: 一个对象就是一个资源, 比如文件, socket, pipes以及网络接口;每个对象都由一个类型标识其用途(file, socket), 并且与一个权限(permissions)集合关联, 该权限集合描述了对象能提供什么样的服务(比如read/write/send等)
  • Object Manager: 对象管理者负责管理所有对象以及这些对象上能够执行的动作
  • Access Vecctor Cache: 用于缓存Security Server的访问决策,以改善系统性能
  • Security Server: 安全服务器根据安全策略来决定某个对象上的动作是否被执行
  • Security Policy: 用于描述SELinux的访问规则
    在这里插入图片描述接下来我们看看SELinux具体是如何给每个对象打标签以及实现安全策略规则的.

标签(label)与策略规则

SELinux是通过标签(label)来匹配执行动作以及策略的.标签决定了何种动作是允许被执行的, socket/文件/进程都有自己的标签. SELinux的访问控制就是根据各个对象上的的标签来决定的, 而策略文件则定义了这些对象是如何相互交互的.

一个标签通常有如下的形式:

user:role:type:mls_level

这样一个标签也通常被成为Security Context。在Android中, 通常不用关心user/role/msl_level, user一般只有u, role对于进程来说是r, 对其他对象是object_r, msl_level是s0, 而type则用来标识对象的类型, 其决定了该对象的所具备的能力, 因此Android中的SELinux又称为基于TE(Type Enforcement)的安全机制, 在Android中, 所有的SELinux策略文件都以te结尾。

通过ls/ps指令中加入-Z参数, 可以查看文件/进程的SELinux状态, 如输入ls -aZ /init*查看init文件夹的标签:
init的标签
上图中的init可执行程序以及文件夹都是在/system/sepolicy/private/file_contexts中定义的:

 # Root
 /                   u:object_r:rootfs:s0
 
 # Data files
 /adb_keys           u:object_r:adb_keys_file:s0
 /build\.prop        u:object_r:rootfs:s0
 /default\.prop      u:object_r:rootfs:s0
 /fstab\..*          u:object_r:rootfs:s0
 /init\..*           u:object_r:rootfs:s0
 /res(/.*)?          u:object_r:rootfs:s0
 /selinux_version    u:object_r:rootfs:s0
 /ueventd\..*        u:object_r:rootfs:s0
 /verity_key         u:object_r:rootfs:s0
 
 # Executables
 /init               u:object_r:init_exec:s0
 /sbin(/.*)?         u:object_r:rootfs:s0

同样输入ps -Z可以查看进程的标签:
在这里插入图片描述
策略规则(policy rules)决定了进程是如何访问对象的, 其通常是如下格式:

allow domains types:classes permissions
  • Domain: 域是一个进程或一组进程的标签,也被成为域类型
  • Type: 对象的标签(如file/socket等)或者一个对象集合
  • Class: 访问对象的类型
  • Permission: 请求的权限(read/write)
    例如:
allow appdomain app_data_file:file rw_file_perms;

这个规则的意思是允许所有应用域的进程访问标签为app_data_file的文件. 所有这些规则需要依赖于global_macros/te_macros的宏定义(位于/system/sepolicy目录下). 除了像上面的规则指定某个特定的域或类型, 也可以通过指定一个属性(attribute)来表示一组域或类型;当通过一个规则有属性时, 会被自动扩展成为了相应的域或类型. 按照上述方式写成的规则如下:

RULE_VARIANT SOURCE_TYPES TARGET_TYPES: CLASSES PERMISSIONS

在这个规则下, 只要一个Subject标识了SOURCE_TYPES就可以有权在类型为CLASSES/标签为TAEGET_TYPES的对象上执行任何在PERMISSONS中声明的操作.例如:

allow domain null_device:chr_file {
    
     getattr open read ioctl write}

这个条规则意思是允许任何有domain域的进程访问null_device类型(对应/dev/null)的字符设备.最后我们来看下Android是如何应用SELinux的.

SELiunx在Android中的应用

Android的SELinux的配置在Android源码中有两个目录:

  • /system/sepolicy
  • /device///sepolicy

而/system/sepolicy主要是Android原生已有的SELinux文件, 包括所有SELinux标签以及策略文件.te的定义, 一般不做修改;/device目录下的SELinux配置通过编译宏BOARD_SEPOLICY_DIRS引入, 所有SELinux相关的编译都要依靠/system/sepolicy/Android.mk这个makefile. 具体来说, SELinux的配置大致有如下几个部分

配置目录 说明
/system/sepolicy/public 包含了系统sepolicy相关的API
/system/sepolicy/private 包含了系统sepolicy的具体实现(与vendor无关)
/system/sepolicy/vendor 提供给厂商(vendor)自由实现的配置
BOARD_SEPOLICY_DIRS 包含厂商sepolicy的定制化配置

所有以.te结尾的都是安全策略文件, 其定义了对象的域(domain)和类型(types); 而SELinux标签文件(也称为SELinux context文件), 大致有如下几种:

  • file_contexts: 为用户空间的文件分配标签
  • genfs_contexts: 为不支持扩展属性的文件分配标签(如proc/vfat)
  • property_contexts: 为Android所有属性分配标签,init进程在初始化时会读取该配置
  • service_contexts: 为Android所有binder服务分配标签, 用于控制哪些进程可以注册/查找这些服务
  • seapp_contexts: 为/data/data目录下的应用分配标签, 应用启动时zygote进程以及在应用安装时installd都会读取该配置
  • mac_permissions.xml: 根据应用的签名(也可能包括包名)分配seinfo tag;seinfo tag在seapp_contexts文件中可以当作一个密钥用于分配特定的标签给所有的应用. 该配置在system_sever启动时会被读取。

那么, 这些SELinux的标签配置以及策略文件是如何编译的? 大致有两个编译路径,所有的file_contexts标签文件都会编译生成一个file_contexts.bin;而其他的如security_classes/*.te/genfs_contexts/port_contexts等文件都会编译生成一个sepolicy的二进制文件, 整体的编译逻辑如下图所示:

编译逻辑
有关SELinux在Android的编译可以参考https://source.android.com/security/selinux/build.

有了SELinux的基础知识, 要如何修改或者添加SELinux规则? 一般, 通过dmesg | grep avc或则logcat | grep avc查看系统当前的SELinux访问的记录, 如果出现avc: denied等字样, 说明有进程违反了安全策略:。

  • 案例1
[   42.357295] selinux: avc:  denied  {
    
     set } for property=net.usb0.dns1 pid=473 uid=0 gid=0 scontext=u:r:network_manager:s0 tcontext=u:object_r:system_prop:s0 tclass=property_service permissive=1

这个访问拒绝的提示说明, 标签为u:r:network_manager:s0的进程473(network_manager)不具备设置标签为u:object_r:system_prop:s0的属性值。
需要添加安全规则:

system/sepolicy/private/network_manager.te

set_prop(network_manager, system_prop)

prebuilts/api/{当前系统版本}/private/network_manager.te

set_prop(network_manager, system_prop)

再次编译验证后就不会出现访问拒绝的日志了.

  • 案例2
avc:  denied  {
    
     set } for property=com.af.test pid=2026 uid=10115 gid=10115 scontext=u:r:platform_app:s0 tcontext=u:object_r:default_prop:s0 tclass=property_service permissive=1

这个访问拒绝的提示说明, 标签为u:r:platform_app:s0的进程2026(platform_app)不具备设置标签为u:object_r:default_prop:s0的属性值。

system/sepolicy/private/property_contexts

#声明属性变量的类型为system_prop
com.af. u:object_r:system_prop:s0

prebuilts/api/{当前系统版本}/private/property_contexts

#声明属性变量的类型为system_prop
com.af. u:object_r:system_prop:s0

system/sepolicy/private/network_manager.te

#set_prop(platform_app , system_prop)
allow platform_app system_prop:property_service {
    
     set };

prebuilts/api/{当前系统版本}/private/network_manager.te

#set_prop(platform_app , system_prop)
allow platform_app system_prop:property_service {
    
     set };

再次编译验证后就不会出现访问拒绝的日志了.

参考:https://sniffer.site/2019/12/07/selinux%E5%9C%A8android%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8/#/%E6%A0%87%E7%AD%BE-label-%E4%B8%8E%E7%AD%96%E7%95%A5%E8%A7%84%E5%88%99