SELinux demonstration using a malicious backdoored daemon

Introduction

SELinux is an abbreviation for Security-Enhanced Linux. It is a rather complicated system that consists of a kernel module and a set of userspace tools for management and control.

According to my understanding, SELinux started its life as a research project called FLASK. From there, it was ported to Linux by the United States' National Security Agency (NSA). In the beginning with Linux, even many experts considered SELinux quite difficult to use and understand, but nowadays the SELinux userspace tools are more sophisticated and helpful to the system administrators. Using SELinux is no longer as hard as it perhaps used to be.

SELinux offers Mandatory Access Control (MAC) to the Linux operating system. What is MAC? Wikipedia defines it as follows:

With mandatory access control, [...] security policy is centrally controlled by a security policy administrator; users do not have the ability to override the policy and, for example, grant access to files that would otherwise be restricted. By contrast, discretionary access control (DAC), which also governs the ability of subjects to access objects, allows users the ability to make policy decisions and/or assign security attributes.

So MAC allows the system administrator to dictate security related rules (called policies in SELinux jargon) that users cannot bypass. With DAC, Discretionary Access Control, users can use commands such as chown, chmod and setfacl to modify access rights in the file system.

At some point Linus Torvalds decided that it is not good to have several different, incompatible patch sets that attempt to enhance Linux OS security. That is why Linux Security Module (LSM) framework was created. LSM is simply a pretty thin abstraction layer in the kernel that allows different security modules to serve as plugins that make security related access decisions. SELinux is implemented as an LSM module and it ships with Red Hat Enterprise Linux and Fedora Linux. AppArmor is also an LSM module, but it is very different from SELinux. So the idea of LSM framework is to allow different security module plugins to be used in the kernel. The LSM framework is quite light-weight, so I guess there is practically no observable performance penalty using LSM.

Backdoored demonstration daemon

To give a concrete and practical example of what SELinux can do for us, I have written a very small backdoored demonstration daemon (bddaemon). As the name suggests, this program is a Linux server program that has a nasty built-in backdoor that allows /bin/bash root shell access to the attacker. However, my bddaemon binds to localhost only (IPv4 127.0.0.1, TCP port 9999). So if you install this daemon for learning purposes, you need not worry: the port 9999 is accessible only from the same host as the daemon is running on.

The RPM package I provide for Fedora Linux 27 contains just /usr/local/sbin/bddaemon and a systemd unit file /usr/lib/systemd/system/bddaemon.service for starting and stopping the daemon. SELinux has a concept called domain transitions that applies between subjects becoming other subjects. Subjects in this context mean processes or threads. With SELinux, when a process does fork() and exec() system calls to execute a new program, it must do so in a manner according to the security policy. For example, you must never start /usr/local/sbin/bddaemon by executing the file directly as a superuser. Doing so could result in a wrong domain transition: the daemon might enter the unconfined domain and after that SELinux security checks would by bypassed. This could happen, for example, in the Fedora Linux 27 where targeted policy is the default. "Targeted policy" is a name invented by Red Hat Linux and it means that the whole system is not under SELinux control. The root user, for example, is pretty free to do whatever he or she wants on Fedora Linux. So the correct way to start the system daemons such as bddaemon is to always do it via systemd:

systemctl start bddaemon

It will do the right thing with domain transitions.

Restricting bddaemon with a SELinux policy

I am by no means an expert, but many years ago I have spent some time studying the SELinux kernel module and the LSM framework that it uses. I plan to write a more detailed article about the SELinux workings later. It is now Sunday, March 18th, 2018 and I toiled the entire yesterday trying to figure out how to make a SELinux policy to restrict bddaemon. I first tried a wrong approach and wanted to modify the entire selinux-targeted-policy that is shipped with Fedora Linux 27. That turned out to be somewhat foolish and it was also very time consuming, since I had to rebuild the RPM with each new attempt at making a policy. Giving the command:

dnf download --source selinux-targeted-policy

and studying the contents was, however, very educational. That command actually downloads selinux-policy source RPM! It is a certain kind of a meta RPM that contains subpackages such as the selinux-targeted-policy. But there is an easier way to control SELinux policies and it is modular. We need not update the whole target policy in one go every time we want to alter the policy.

It is appropriate to give many thanks to Miroslav Grepl at Red Hat Linux. He has lots of SELinux expertise and he is willing to share it with us. I finally got on the right track after reading his article How to create a new initial policy using sepolicy-generate tool?.

As I said earlier, SELinux userspace tools have improved immensely during the past ten years or so, and now I could generate a policy template for bddaemon with simply:

mkdir my_policy
cd my_policy
sepolicy generate --init -n bddaemon /usr/local/sbin/bddaemon

The output was five files named:

  1. bddaemon.te
  2. bddaemon.if
  3. bddaemon.fc
  4. bddaemon.sh
  5. bddaemon_selinux.spec

The first of these, bddaemon.te, contains the Type Enforcement (TE) types and rules. There are allow rules in it that specify what this subject (i.e. process) is allowed to do. By default, all actions not allowed are denied by SELinux. So you will understand that this file is extremely important!

Here lies both the beauty and the horror of SELinux. It can be difficult and time consuming to properly confine a complicated application or daemon, because:

The second file bddaemon.if is partly mysterious to me. I copied some configuration settings from ejabberd.if and possibly also from apache.if. But I must confess I am not sure whether all those settings are needed. The file suffix "if" stands for "Interfaces".

The third file bddaemon.fc is easy to understand. The suffix "fc" stands for "File Contexts". Here you will specify which labels files, directories, Unix domain sockets etc. should have. At the very heart of SELinux is the basic idea of attaching labels to almost everything there is. Using these labels, or security contexts if you want to call them that, SELinux can form rules and restrict access, because it knows the security "tag" or type of every subject (i.e. process) and object (e.g. file) that it sees. Inside the SELinux kernel module, the text labels are converted to unsigned 32-bit integers (SIDs, Security Identifiers) for efficiency. Those numbers waste less kernel memory and are faster to operate on than text strings.

The fourth file bddaemon.sh is a shell script for automating the task of building bddaemon.pp, i.e. the binary policy module that can be loaded into the SELinux enabled Linux kernel. Despite Miroslav Grepls suggestion, I did not execute the file, but I studied it. The script is very small and easy to understand, so I took the basic logic from it and managed to create bddaemon.pp and RPM packages. Even though bddaemon is quite a simple and artificial program, creating all the correct rules did not happen immediately. I had to inspect /var/log/audit/audit.log and use audit2allow to get audit rules. For instance, binding to the TCP port 9999 required a special allow rule that mentions jboss. According to a friend, jboss is some kind of a Java program. Of course bddaemon has nothing to do with it, but the port 9999 is normally associated with that program, and I had to create the following rule:

allow bddaemon_t jboss_management_port_t:tcp_socket name_bind;

The fifth file bddaemon_selinux.spec is a SPEC-file template for creating an RPM package for Fedora Linux 27. The resulting package bddaemon_selinux will contain bddaemon.pp and bddaemon.if, but I am not sure why the latter is there. bddaemon.pp makes perfect sense, since that is the binary format security module that gets loaded into the SELinux enabled Linux kernel.

Steps for testing SELinux

So much for the background. Here are the concrete steps to help you with your SELinux testing:

  1. Install Fedora Linux 27 on a virtual machine such as VirtualBox. Fedora Linux comes with SELinux targeted policy installed and SELinux is in enforcing mode by default. You can verify that by giving command:
    sestatus

    The reply should be like the following:

    SELinux status: enabled
    SELinuxfs mount: /sys/fs/selinux
    SELinux root directory: /etc/selinux
    Loaded policy name: targeted
    Current mode: enforcing
    Mode from config file: enforcing
    Policy MLS status: enabled
    Policy deny_unknown status: allowed
    Memory protection checking: actual (secure)
    Max kernel policy version: 31
  2. Download bddaemon binary RPM for Fedora Linux 27 bddaemon-0.4-1.x86_64.rpm.
  3. Optionally download the source RPM bddaemon-0.4-1.src.rpm if you want compile yourself.
  4. Download bddaemon-0.4.tar.gz. This contains the C source code for bddaemon. Study this and you will understand how the daemon is backdoored. The backdoor is intentional, it is not hidden in any way!
  5. Download the noarch RPM bddaemon_selinux-1.1-1.fc27.noarch.rpm. This contains the SELinux policy module for restricting bddaemon. It is essential to have!
  6. Download bddaemon_policy.tar.gz. This contains the source code for bddaemon.pp. Study this and you will understand how SELinux will restrict bddaemon.
  7. Install bddaemon:
    rpm -Uvh /your/path/to/downloaded/bddaemon-0.4-1.x86_64.rpm
  8. Install bddaemon SELinux policy:
    rpm -Uvh /your/path/to/downloaded/bddaemon_selinux-1.1-1.fc27.noarch.rpm
  9. Start bddaemon:
    systemctl start bddaemon
  10. Use nc (netcat) to talk to bddaemon. To connect, issue command:
    nc 127.0.0.1 9999

    Here is a screen capture of my sample session in Xfce desktop environment that I love. If the connection succeeds, you will see bddaemon welcome banner saying "bddaemon ready". In this session I first wrote command "hi". The daemon replied politely "hello". After that I gave the malicious greeting "howdy" to enable the backdoor. Without SELinux protection, the bddaemon would have executed /bin/bash and attached its file descriptors to the network socket, giving the attacker root shell access to the host! But because we have told SElinux that bddaemon is allowed only network access, writing to system logs and little else, SELinux denies everything else! So the bddaemon malicious code trying to do execle() fails. In this example, we got a GUI notification, but you should also check out /var/log/audit/audit.log to see that SELinux indeed stopped the attack!

    bddaemon

How do you search the audit log? Try the following ausearch command:

ausearch -m avc --context system_u:system_r:bddaemon_t:s0

Your search will match both source and target contexts system_u:system_r:bddaemon_t:s0. That is the SELinux security context for the running /usr/local/sbin/bddaemon. My search found the result we wanted:

time->Sun Mar 18 19:04:58 2018
type=AVC msg=audit(1521392698.114:588): avc: denied { execute } for pid=3531 comm="bddaemon" name="bash" dev="dm-0" ino=794599 scontext=system_u:system_r:bddaemon_t:s0 tcontext=system_u:object_r:shell_exec_t:s0 tclass=file permissive=0

Conclusion

I hope this article showed you how useful SELinux can be. In this case I used an artificial example with a small bddaemon that had an intentional backdoor that is easy to detect. There is not much C code to study and the backdoor is obvious. But in the real world, complicated programs do sometimes contain unintentional programming errors that could lead to attacks similar to the one seen with this tiny demonstration server.

Buffer overruns coupled with executable stacks are well-known examples. SELinux can protect from some userspace programming errors by enabling MAC, Mandatory Access Control. It is implemented in the operating system kernel and its security policy is defined by the system administrator.

In addition to the MAC protection, SELinux also contains MLS, Multi Level Security. But at this moment I am still unsure what it is and how to use it.

Kalevi Kolttonen <kalevi@kolttonen.fi>


UPDATE March 19th, 2018: Today I was very curious about how sd_notify(3) call would work with systemd. It is used to inform systemd that a daemon is ready to serve clients. That requires Type=notify in the systemd unit file for the service instead of Type=forking. So I commented out daemon(3) call from bddaemon.c so that systemd would be able to track this daemon (daemon(3) would do fork(2)).

This small change has absolutely nothing to do with the SELinux demonstration described above. As I said, I just wanted to know how this new model would work and the bddaemon.c code was close at hand. Unfortunately I have now introduced a dependency on systemd-devel on Fedora Linux. Sorry about that.