Android Security: A walk-through of SELinux

In DAC, each process has an owner and belong to one or several groups, and each resource will be assigned different access permission for its owner and group members. It is useful and simple. The problem is once a program gain root privileged, it can do anything. It has only three permissions which you can control, which is very coarse.
SELinux is to fix that.
It is much fine-grained. It has lots of permissions defined for different type of resources. It is based on the principle of default denial. We need to write rules explicitly state what a process, or a type of process (called domain in SELinux), are allowed to do. That means even root processes are contained. A malicious process belongs to no domain actually end up can do nothing at all. This is a great enhancement to the DAC based security module, and hence the name Security-Enhanced Linux, aka SELinux.
In this article, we'll have a walk-through of various components of SELinux on Android. We will take a top-down approach. The reason is SELinux has lots of concepts to understand, and files to manipulate, so quite likely, you will won't get the big picture if I were to start with the details first.
However, it is also possible that this approach will create more confusion. But let's try anyway.

How SElinux works

The core idea of SELinux is to label every resources and process, and on the base of default denial, to craft explicit rules granting a process certain permissions to access the resources.
Let's break it down.
  1. Define resource types, and label the resources
  2. Define process domains, and label the processes
  3. Write rules that grant a process the permissions
  4. Rules in action.

Define resource types, and label the resources

Define resource types

Use the type keyword and declare it in a .te file. The suffix te stands for Type Enforcement and no surprise, type are defined in this file.
Specifically, in Android:
  • Types for (normal) files are defined in the /system/sepolicy/file.te.
  • Types for devices files are defined in the /system/sepolicy/device.te.
  • Types for executables files are defined in individual domain files, e.g /system/sepolicy/mediaserver.te for mediasever domain.
Here is the example for each type of te files:
Code snippet of file.te:
# Filesystem types
type labeledfs, fs_type;
type pipefs, fs_type;
type sockfs, fs_type;
type rootfs, fs_type;

# proc, sysfs, or other nodes that permit configuration of kernel
type proc_bluetooth_writable, fs_type;
type proc_cpuinfo, fs_type;
type proc_iomem, fs_type;
type proc_meminfo, fs_type;
type proc_net, fs_type;

type adb_data_file, file_type, data_file_type;
Code snippet of device.te:
type audio_device, dev_type;
type binder_device, dev_type, mlstrustedobject;
type block_device, dev_type;
type camera_device, dev_type;
Code snippet of mediaserver.te, an example domain te file. (Domain te files also include other important things but at this moment, we'll focus on the type definition only.)
#/system/sepolicy/mediaserver.te:
type mediaserver_exec, exec_type, file_type;
A few takeaways:
  • It is really a very fine-grained type system. Take a look at those different types of proc_xxx. Because of this fine-grained labeling, we can write accurate rules that will only allow a process to access a very narrow subset of resources, or even a single file, when that type labels only a single file.
  • It is targeted and designed for Android. See the adb_data_file.

Label the resources

To label a resource is also called to create a Security Contexts for a resource, or file contexts. The file contexts common to all devices are put in system/sepolicy/file_contexts. Vendor specific file contexts should reside in device/vendor/device/sepolicy. They will be combined together to generate the final file context during the build process.
Let's take a look at the common file_contexts.
# system/sepolicy/file_contexts
# truncated to show an overall view

# Root
/fstab\..*          u:object_r:rootfs:s0
/init\..*           u:object_r:rootfs:s0
/ueventd\..*        u:object_r:rootfs:s0

# Dev
/dev/audio.*        u:object_r:audio_device:s0
/dev/binder         u:object_r:binder_device:s0

# System
/system/bin/mediaserver     u:object_r:mediaserver_exec:s0
/system/bin/servicemanager  u:object_r:servicemanager_exec:s0
/system/bin/surfaceflinger  u:object_r:surfaceflinger_exec:s0

# Vendor
/vendor(/.*)?               u:object_r:system_file:s0
/vendor/bin/gpsd            u:object_r:gpsd_exec:s0

# ODM/OEM
/odm(/.*)?                  u:object_r:system_file:s0
/oem(/.*)?                  u:object_r:oemfs:s0

# Data
/data/security(/.*)?  u:object_r:security_file:s0
/data/drm(/.*)?   u:object_r:drm_data_file:s0
/data/gps(/.*)?   u:object_r:gps_data_file:s0
A few takeaways:
  • It labels everything : normal file, device file, executables.
  • It labels everywhere : rootfs, system, vendor, data partitions.
  • It is tailed for Android, sometime its called SEAndroid.
The label follows form of user:role:type:sensitivity. From the example shown above, you can tell type is the most important part, since the other part are same in Android at the moment.
To check the context for files use ls -Z
$ ls -Z /system/bin/mediaserver
u:object_r:mediaserver_exec:s0 /system/bin/mediaserver

Define process domains, and label processes

Domain is similar to Type but used to label a process, instead of a file. To define a domain, use type keyword as well, but the second parameter is domain instead of xxx_type. See line (1) in following code snippet.
/system/sepolicy/mediaserver.te:
    # mediaserver - multimedia daemon
    type mediaserver, domain, domain_deprecated;          (1)
    type mediaserver_exec, exec_type, file_type;          (2)  

    net_domain(mediaserver) 
    init_daemon_domain(mediaserver)                       (3)
However, labeling a process with a Domain is different from labeling the corresponding executable file (with a Type). For example, we know from preceding introduction that the /system/bin/mediaserver is labeled as u:object_r:mediaserver_exec:s0, and that's down to the mediaserver_exec type.
But, the domain of the process mediaserver of is mediaserver. And, we'll see late, when writing the rules, we will use domain mediasever, instead of mediaserver_exec.
$ ps -Z | grep mediaserver
u:r:mediaserver:s0    media   1662  1 44340  10456 /system/bin/mediaserver
It's not hard to imagine there is something done under the hood creating this association (or transition) between the exec type and process domain. That's done through the macro in line (3). For now, we can just pretend we know what it is.

Write rules that will apply to a process and resources.

A rule will roughly say "allow somebody do something on something". Or better, "allow a subject take some actions on an object". The subject here is the process; the object here are the resources, such files, sockets; and the actions are the permissions. We have already know how to label the subject and object, in order to write a rules, we also need to define the permission.

Permissions

In DAC, we have three access permissions, read, write, execute, for all type of files. In SELinux, things are much more complex, for a good reason.
SELinux (or SEAndroid) defines a list of types, called security classes. Its includes File, Directory, File System, Socket, Processes, security and capability. A full list can be found at system/sepolicy/security_class.
For different classes, there are different permissions, or called access vectors. A full list can be found at system/sepolicy/access_vectors.
An simple explanation of the permissions defined by SELinux can be found here.
Among those security classes, there is a class called Process, which defines the permission a process itself can do, for example, whether the process is allowed to change the security context of its own and the child processes. This constraint can be done in DAC based model.

Final, write the policies.

Policy files is the place to put all the pieces together and do the ultimate job of imposing access control.
The policy file ends with .te suffix as well, same as the type definition files. They are organized by domains, for example bluetooth.te for bluetooth domain/service/daemon, mediaserver.te for mediaserver domain/service/daemon.
The format of the rule is:
rule_name source_type target_type : class perm_set;
Below is truncated code of mediaserver.te. It shows an example of how to control the access of different type of resources: files, directories, devices, sockets, process, and binder services. Most of the rules are (hopefully) self explanatory, if you had followed the discussion (and congratulation on that). What worth note is the ability to control the out going bind calls. It means you have add that explicitly in the policy file, if you want to use a new binder service, or call a new API of a binder service. This is really tough, and some may consider it is cumbersome. However, it makes the system more secure. Everything vital permission must be explicated allowed. That is the core idea of SELinux, or Mandatory Access Control (aka MAC) in general.
# /system/sepolicy/mediaserver.te:
# truncated, an example for different type of permissions: 

# files
allow mediaserver media_data_file:file create_file_perms;
# dirs
allow mediaserver oemfs:dir search;
# devices
allow mediaserver gpu_device:chr_file rw_file_perms;
allow mediaserver video_device:dir r_dir_perms;
# socket
allow mediaserver rild:unix_stream_socket { connectto read write setopt };
# process
allow mediaserver self:process ptrace;
# bind service
allow mediaserver activity_service:service_manager find;
use_drmservice(mediaserver)
allow mediaserver drmserver:drmservice { setPlaybackStatus openDecryptSession }
allow is the most used rule, and that is conform to the selinux's default denial model. There is also other rules, such as neverallow. This rule specifies that an allow rule must not be generated for the operation, even if it has been previously allowed.
For example, app.te states that app are never allowed to access the hardware device files. It is also a requirement in CTS, the neverallow rules in system/sepolicy are never allowed to be modified.
system/sepolicy/app.te
# Access to any of the following character devices.
neverallow appdomain {
    audio_device
    camera_device                                            
    dm_device
    gps_device
    radio_device
    rpmsg_device
    video_device
}:chr_file { read write };

Rules in actions

sepolicy(.bin), file_context.bin and policy.conf

We have discussed a bunch of different files that are used to define the sepolicy. .te files are used either to define type , or rules; file_contexts are used to label the resources. Those are all human readable files, for obvious reason. And, not surprise, a binary representation will be generated from those file for efficiency. Among the binary files, two important ones are sepolicy(.bin) and file_contexts.bin, which are all end up in the root file system during the build process. And they will be used by selinux to enforce the rules.
The seplicy(.bin) is the output of checkpolicy, with an intermediate file called policy.conf as the input. In turn, the policy.conf is an aggregation of all the *.tefiles, among others. And, a good news is that policy.conf is text based, so we can diff that to diagnose policy issues between two version change.
Apart from the sepolicy.bin and file_contexts.in, there are a few other files will be installed in either the rootfs or system partition during the build process, such as seapp_contexts, service_contexts and mac_permissions.xml. But we won't go details in this tutorial.

init and selinux in kernel

We have all the labels and rules in place but we haven't really set up anything yet. Say, apply a label for a file. That is taken care by the init program.
Apart from the well know functionality such as read the init.rc, mount the partitions, and start the services, init also responsible for the initiation of SELinux. It includes set up the labels for all the files according to the file_contexts.bin and pass the sepolicy to the kernel, among other things. Grepselinux_ in system/core/init/init.cpp for all the gory details.
How the SELinux policy is actually enforced by the kernel is outside of the scope of this tutorial, but at least make sure the relevant configuration is enabled.

Summary

In this article, we start with why SELinux is introduced and the core idea of it - label every thing, default denial, and explicit rules to allow anything. Then we look at the various components and steps need to implement that strategy, in Android.
Hopefully at the end of the introduction, you are convinced that SELinux is a very effective way, if not the most effective way, to protect the device security. And I think SELinux should be mandatory on all the devices, be it mobile devices, IoT devices or servers. Of course, that means they have to run Linux first, which is nice :)
For other stuff such as enable/disable SElinux, how to interpret and fix the SELinux warning (svc message), you can check this official documentation.