Adding Services to Domains - Exploring SE for Android (2015)

Exploring SE for Android (2015)

Chapter 9. Adding Services to Domains

In the previous chapter, we covered the process of getting file objects in the proper domain. In most cases, the file object is the target. However, in this chapter, we will:

· Emphasize labeling processes—notably Android services run and managed by init

· Manage the ancillary associated objects created by init

Init – the king of daemons

The init process is vital in a Linux system, and Android is not special in this case. However, Android has its own implementation of init. Init is the first process on the system, and thus has a Process ID (PID) of 1. All other processes are the result of a direct fork()from init, thus all processes eventually are parented under init, either directly or indirectly. Init is responsible for cleaning up and maintaining these processes. For instance, any child process whose parent dies is reparented under init by the kernel. In this way, init can call wait() (man 2 wait for more details) to clean up after the process when it exits.

Note

A process which has terminated but has not had wait() called is a zombie process. The kernel must keep the process data structures around until this call. Failing to do so will consume memory indefinitely.

Since init is the root of all processes, it also provides a mechanism to declare and execute commands through its own scripting language. Files using this language to control init are referred to as init scripts, and we have already modified some of them. In the source tree, we used the init.rc file, which you can reach by navigating to device/fsl/imx6/etc/init.rc, but on the device, it is packaged with the ramdisk at /init.rc, and is made available to init, which is also packaged in the ramdisk at /init.

To add a service to the init script, you can modihe init.re and add a declaration, as follows:

service <name> <path> [ <argument>... ]

Here, name is the service name, path is the path to the executable, and argument are space delimited argument strings to be delivered to the executable in its argv array.

For example, here is the service declaration for rild, the Radio Interface Layer Daemon (RILD):

Service ril-daemon /system/bin/rild

It is often the case that additional service options can and need to be added. The init script service statement supports a rich assortment of options. For the complete list, refer to the informational file located at system/core/init/readme.txt. Additionally, we covered the SE for Android-specific changes in Chapter 3, Android Is Weird.

Continuing to dissect rild, we see that the rest of the declaration in the UDOO init.rc is as follows:

Service ril-daemon /system/bin/rild

class main

socket rild stream 660 root radio

socket rild-debug stream 660 radio system

socket rild-ppp stream 660 radio system

user root

group radio cache inet misc audio sdcard_rw log

The interesting thing to note here is that it creates quite a few sockets. The socket keyword in init.rc is described by the readme.txt file:

Note

From the source tree file system/core/init/readme.txt:

socket <name> <type> <perm> [ <user> [ <group> [ <context> ] ] ]

Create a Unix domain socket named /dev/socket/<name> and pass its fd to the launched process. The type must be dgram, stream, or seqpacket. The user and group IDs default to 0. The SELinux security context for the socket is context. It defaults to the service security context, as specified by seclabel, or is computed based on the service executable file's security context.

Let's take a look at this directory and see what we've found.

root@udoo:/dev/socket # ls -laZ | grep adb

srw-rw---- system system u:object_r:adbd_socket:s0 adbd

This raises the question, "How did it get into that domain?" Using our knowledge from the previous chapter, we know that / dev is a tmpfs, so we know that it did not enter this domain through xattrs. It must be either a code modification or a type transition. Let's check whether it's a type transition. If it is, we would expect to see a statement in the expanded policy.conf. SELinux policy is based on the m4 macro language. During builds, it is expanded into policy.conf, and then compiled. Chapter 12, Mastering the Tool Chain, has more details on this.

We can discover this by using sesearch to find type transitions for adbd_socket:

$ sesearch -T -t adbd_socket $OUT/sepolicy

As you can see from the empty output, there are zero such lines, so it's not the policy which is doing this but a code change.

In Linux, processes are created with fork() followed by exec(). Because of this, we are able to afford great keywords to search the init daemon. We suspect that the code to set up the socket is just after a call to fork() in the child processes and before a call toexec():

$ grep -n fork system/core/init/init.c

235: pid = fork();

So, the fork we are searching for is on line 235 of init.c; let's open init.c in a text editor and take a look. We will find the following snippet to examine:

...

NOTICE("starting '%s'\n", svc->name);

pid = fork();

if (pid == 0) {

struct socketinfo *si;

struct svcenvinfo *ei;

char tmp[32];

int fd, sz;

umask(077);

if (properties_inited()) {

get_property_workspace(&fd, &sz);

sprintf(tmp, "%d,%d", dup(fd), sz);

add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);

}

for (ei = svc->envvars; ei; ei = ei->next)

add_environment(ei->name, ei->value);

for (si = svc->sockets; si; si = si->next) {

int socket_type = (

!strcmp(si->type, "stream") ? SOCK_STREAM :

(!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));

int s = create_socket(si->name, socket_type,

si->perm, si->uid, si->gid, si->socketcon ?: scon);

if (s >= 0) {

publish_socket(si->name, s);

}

...

According to man 2 fork, the return code of fork() in the child process is 0. The child process executes within this if statement and the parent skips it. The function create _ socket() also seems interesting. It appears to take the name of the service, the type of socket, permissions flags, uid, gid, and socketcon. What is socketcon? Let's check whether we can trace back to where it is set.

If we look before fork(), we can see that the parent process gets its scon based on two factors:

...

if (svc->seclabel) {

scon = strdup(svc->seclabel);

if (!scon) {

ERROR("Out of memory while starting '%s'\n", svc->name);

return;

}

} else {

...

The first path through the if statement occurs when svc->seclabel is not null. This svc structure is populated with the options that can be associated with a service. As a refresher from Chapter 3, Android Is Weird, seclabel lets you explicitly set the context on a service, hardcoded to the value in init.rc. The else clause is a bit more involved and interesting.

In the else clause, we get the context of the current process by calling getcon(). This function, since we're running in init, should return u:r:init:s0 and store it in mycon. The next function, getfilecon() is passed the path of the executable, and checks the context of the file itself. The third function is the workhorse here: security_compute_create(). This takes the mycon, fcon, and target class and computes the security context, scon. Given these inputs, it tries to determine, based on policy type transitions, what the resulting domain for the child should be. If no transitions are defined, scon will be the same as mycon.

A conditional expression within the create_socket() function additionally determines the socket context passed. The variable si is a structure that contains all the options to the socket statement in the init service section. As specified by the readme.txt file, si->socketcon is the socket context argument. In other words, the socket context can come from one of three places (in descending priority):

· The socketcon option on the socket option in the service declaration

· The seclabel option on the service keyword

· Dynamically computed from source and target contexts

The socket context is passed to create_socket(). Now, let's look at create_socket(). This function is defined at system/core/init/util.c:87. The snippets of code around socket() seem interesting:

...

if (socketcon)

setsockcreatecon(socketcon);

fd = socket(PF_UNIX, type, 0);

if (fd < 0) {

ERROR("Failed to open socket '%s': %s\n", name, strerror(errno));

return -1;

}

if (socketcon)

setsockcreatecon(NULL);

...

The setsockcreatecon() function sets the process' socket creation context. This means that the socket created by the socket() call will have the context set via setsockcreatecon(). After it's created, the process resets it to the original by using setsockcreatecon(NULL).

The next bit of interesting code is around bind():

...

filecon = NULL;

if (sehandle) {

ret = selabel_lookup(sehandle, &filecon, addr.sun_path, S_IFSOCK);

if (ret == 0)

setfscreatecon(filecon);

}

ret = bind(fd, (struct sockaddr *) &addr, sizeof (addr));

if (ret) {

ERROR("Failed to bind socket '%s': %s\n", name, strerror(errno));

goto out_unlink;

}

setfscreatecon(NULL);

freecon(filecon);

...

Here, we have set the file creation context. The functions are analogous to setsock_creation(), but work for filesystem objects. However, the selabel_lookup() function looks in file_contexts for the context of the file. The part you might be missing is that the call tobind(), for path-based sockets, creates a file at the path specified in sockaddr_un struct. So, the socket object and the filesystem node entry are distinctly separate things and can have different contexts. Typically, the socket belongs to the process' context, and the filesystem node is given some other context.

Dynamic domain transitions

We saw init computing of the contexts for the init sockets, but we never encountered it while setting the domains for child processes. In this section, we will dive into the two techniques to do so: explicit setting with an init script and sepolicy dynamic domain transitions.

The first way to the domains for child processes is with the seclabel statement in the init script service declaration. Within the child processes execution after fork(), we find this statement:

if (svc->seclabel) {

if (is_selinux_enabled() > 0 && setexeccon(svc->seclabel) < 0) {

ERROR("cannot setexeccon('%s'): %s\n", svc->seclabel, strerror(errno));

_exit(127);

}

}

To clarify, the svc variable is the structure that contains the service options and arguments, so svc->seclabel is seclabel. If it's set, it calls setexeccon(), which sets the process' execution context for anything it executes via exec(). Further down, we see that the exec()function calls are made. The exec() syscall never returns on success; it only returns on failure.

The other way to set the domains for child processes, which is the preferred way, is by using sepolicy. It's preferred because the policy has no dependencies on anything else. By hardcoding a context into init, you're coupling a dependency between the init script and the sepolicy. For instance, if the sepolicy removes a type that was hardcoded in the init script, the init setcon will fail, but both systems will compile correctly. If you remove a type for a type transition and leave the transition statement, you can catch the error at compile time. Since we looked at the rild service statement, let's look at the rild.te policy file located in sepolicy. We should search for the type_transition keyword in this file using grep:

$ grep -c type_transition rild.te

0

No instances of type_transition are found, but this keyword must exist, similar to files. However, it can be hidden in an unexpanded macro. The SELinux policy files are in the m4 macro language, and they get expanded prior to being compiled. Let's look throughrild.te and check whether we can find some macros. They are distinguished and look like functions with parameters. The first macro we come across is the init_daemon_domain(rild) macro. Now, we need to find this macro's definition in sepolicy. The m4 language uses the define keyword to declare macros, so we can search for that:

$ grep -n init_daemon_domain * | grep define

te_macros:99:define(`init_daemon_domain', `

Our macro is declared in te_macros, which coincidentally holds all the macros related to type enforcement (TE). Let's take a look at what this macro does in more detail. First, its definition is:

...

#####################################

# init_daemon_domain(domain)

# Set up a transition from init to the daemon domain

# upon executing its binary.

define(`init_daemon_domain', `

domain_auto_trans(init, $1_exec, $1)

tmpfs_domain($1)

')

...

The commented lines in the preceding code (lines starting with # in m4), state that it sets up a transition from init to the daemon domain. This sounds like something we want. However, both the encompassing statements are macros, and we need to recursively expand them. We will start with domain_auto_trans():

...

#####################################

# domain_auto_trans(olddomain, type, newdomain)

# Automatically transition from olddomain to newdomain

# upon executing a file labeled with type.

#

define(`domain_auto_trans', `

# Allow the necessary permissions.

domain_trans($1,$2,$3)

# Make the transition occur by default.

type_transition $1 $2:process $3;

')

...

The comment here indicates that we are headed in the proper direction; however, we need to keep expanding macros in our search. According to the comment, the domain_trans() macro allows just the transition to occur. Remember that almost everything in SELinux needs explicit permission from the policy in order to happen, including type transitions. The last statement in the macro is the one we were searching for:

type_transition $1 $2:process $3;

If you expand this statement out, you'll get:

type_transition init rild_exec:process rild;

What this statement conveys is that if you make an exec() syscall on a file with the type rild_exec, and the executing domain is init, then make the child process' domain rild.

Explicit contexts via seclabel

The other option for setting contexts is very straightforward. It's hardcoding them with the init script in the service declaration. In the service declaration, as we saw in Chapter 3, Android Is Weird, there were modifications to the init language. One of the additions isseclabel. This option just lets init explicitly change the context of the service to the argument given to seclabel. Here is an example of adbd:

Service adbd /sbin/adbd

class core

socket adbd stream 660 system system

disabled

seclabel u:r:adbd:s0

So why use dynamic transitions on some and seclabel on others? The answer is dependent on where you're executing from. Things such as adbd execute early on from the ramdisk, and since the ramdisk really doesn't use per file labels, you can't set up transitions properly—the target has the same context.

Relabeling processes

Now that we are armed with dynamic process transitions, and the ability to set socket contexts from init scripts is needed. Let's attempt to relabel the services that are in improper contexts. We can tell if they're improper by checking them against the following rules:

· No other process but init should be in the init context

· No long running process should be in the init_shell domain

· Nothing but zygote should be in the zygote domain

Note

A more comprehensive test suite is part of CTS on AOSP. Refer to the Android CTS project for more details: (git clone) https://android.googlesource.com/platform/cts. Take note of the ./hostsidetests/security/src/android/cts/security/SELinuxHostTest.java and./tests/tests/security/src/android/security/cts/SELinux.*.java tests.

Let's run some basic commands and evaluate the status of our UDOO over the adb connection:

$ adb shell ps -Z | grep init

u:r:init:s0 root 1 0 /init

u:r:init:s0 root 2267 1 /sbin/watchdogd

u:r:init_shell:s0 root 2278 1 /system/bin/sh

$ adb shell ps -Z | grep zygote

u:r:zygote:s0 root 2285 1 zygote

We have two processes in the improper domains. The first is watchdogd, and the second is a sh process. We need to find these and correct them.

We will start with the mystery sh program. As you can recall from the previous chapter, our UDOO serial console process had the context of init_shell, so this is a good suspect. Let's check PIDs and find out. From a UDOO serial console execute:

root@udoo:/ # echo $$

2278

We can compare this PID to the PID field in the adb shell ps output here (PID field is the third field, index 2), and as you can see, we have a match.

From there, we need to find the service declaration for this. We know that it is in init.rc since it's running in init_shell, a type that can only be transitioned to by init directly as per the SELinux policy. Also, init only starts processing things by service declarations, so in order to be in init_shell, you must start by init via a service declaration.

Note

Use sesearch to find out such things on the compiled sepolicy binary:

$ sesearch -T -s init -t shell_exec -c process $OUT/root/sepolicy

If we search init.rc for the UDOO, which is in udoo/device/fsl/imx6/etc, we can grep its contents for /system/bin/sh, the command in question. If we do that, we will find:

$ grep -n "/system/bin/sh" init.rc

499:service console /system/bin/sh

702:service wifi_mac /system/bin/sh /system/etc/check_wifi_mac.sh

Let's look at 499 since we don't have anything to do with Wi-Fi:

service console /system/bin/sh

class core

console

user root

group root

If this is the service in question, we should be able to disable it, and verify that our serial connection no longer works:

$ adb shell setprop ctl.stop console

My live serial connection died at:

root@udoo:/ # avc: denied { set } for property=ctl.console scontext=u:r:shell:s0 tcontext=u:e

Now that we have verified what it is, we can start it back up:

$ adb shell setprop ctl.start console

With the system back in a working state, we now need to address the best way to correct the label on this service. We have two options:

· Using an explicit seclabel entry in init.rc

· Using a type transition

The option we will use here is the first. The reason is because init executes shell from time to time, and we don't want all of these in the console processes domain. We want least privilege to segregate the running processes. By using the explicit seclabel, we won't change any of the other shells that are executed along the way.

To do this, we need to modify the init.rc entry for console; add:

service console /system/bin/sh

class core

console

user root

group root

seclabel u:r:shell:s0

The proper domain for this executable is shell, since it should have the same permission set as adb shell. After you make this change, recompile the bootimage, flash, and then reboot. We can see that it is now in a shell domain. To verify, execute the following from a UDOO serial connection:

root@udoo:/ # id -Z

uid=0(root) gid=0(root) context=u:r:shell:s0

Alternatively, execute the following command using adb:

$ adb shell ps -Z | grep "system/bin/sh"

u:r:shell:s0 root 2279 1 /system/bin/sh

The next one we need to take care of is watchdogd. The watchdogd process already has a domain and allows rules in watchdog.te; so we just need to add a seclabel statement and get it into this proper domain. Modify init.rc:

# Set watchdog timer to 30 seconds and pet it every 10 seconds to get a 20 second margin

service watchdogd /sbin/watchdogd 10 20

class core

seclabel u:r:watchdogd:s0

To verify using adb, execute the following command:

$ adb shell ps -Z | grep watchdog

u:r:watchdogd:s0 root 2267 1 /sbin/watchdogd

At this point, we have made actual policy corrections that the UDOO was in need of. However, we need to practice the use of dynamic domain transitions. A good teaching example would have subshells from a shell in their own domain. Let's start by defining a new domain and setting up the transition.

We will create a new .te file in sepolicy called subshell.te, and edit it so that its contents contain the following:

type subshell, domain, shelldomain, mlstrustedsubject;

# domain_auto_trans(olddomain, type, newdomain)

# Automatically transition from olddomain to newdomain

# upon executing a file labeled with type.

#

domain_auto_trans(shell, shell_exec, subshell)

Now, the mmm trick used earlier in the book can be used to compile just the policy Also, use adb push command to push the new policy to /data/security/current/sepolicy and execute setprop to reload the policy, just as we did in Chapter 8, Applying Contexts to Files.

To test this, we should be able to type sh, and verify the domain transition. We will start by getting our current context:

root@udoo:/ # id -Z

uid=0(root) gid=0(root) context=u:r:shell:s0

Then execute a shell by doing:

root@udoo:/ # sh

root@udoo:/ # id -Z

uid=0(root) gid=0(root) context=u:r:subshell:s0

We were able to use a dynamic type transition to get a new process in a domain. If you couple this with labeling files, as presented in Chapter 8, Applying Contexts to Files, you have a powerful tool to control process permissions.

Limitations on app labeling

A fundamental limitation of these dynamic process transitions is that they require an exec() system call to be made. Only then can SELinux compute the new domain, and trigger the context switch. The only other way to do this is by modifying the code, which essentially is what init is doing when you specify seclabel(). The init code sets the exec context for its process, causing the next exec to end up in the specified domain. In fact, we can see this in the init.c code:

if (svc->seclabel) {

if (is_selinux_enabled() > 0 && setexeccon(svc->seclabel) < 0) {

ERROR("cannot setexeccon('%s'): %s\n", svc->seclabel, strerror(errno));

_exit(127);

}

}

Here, the child process gets its execute context set by a call to setexeccon() before the exec() system call hands over control to a new binary image. In Android, applications are not spawned this way, and no exec() syscall exists in the process creation path; so a new mechanism will be needed.

Summary

In this chapter, we learned how to label processes via type transitions as well as via the seclabel statements. We also investigated how init manages service sockets, and how to properly label them. We then corrected the process contexts for the serial console as well as the watchdog daemon.

Applications in Android never have an explicit call to exec() to start their program execution. Since there is no exec(), we have to label applications with a code change. In the next chapter, we will address how this happens, and how applications get labeled.