zathura - SELinux confined

11 minute read

About

When hardening a Linux workstation, it is important to isolate and control the applications. There are several tools available, but none as powerful as SELinux.

zathura is a customizable and minimalistic document viewer which supports PDF documents and other file formats.

This blog post describes how to securely confine the zathura application to a SELinux domain.

Threat model

zathura supports PDF documents by loading the poppler rendering engine plugin.

poppler is a complete implementation of ISO 32000-1 (Portable document format), and supports both simple and complex PDF documents. Complex documents may contain annotations, forms, multimedia and even dynamic code.

Arbitrary code execution

Complex PDF documents can possibly trigger parsing vulnerabilities in the rendering engine. A method to detect parsing errors is fuzzing, and poppler is continuously scanned with oss-fuzz.

The have been several findings, some which have resulted in denial of service vulnerabilities.

Also, there are several heap overflow bugs, some which could be exploitable.

In addition, zathura can execute shell commands directly from the application. This is convenient, but may not be considered to be core functionality for a PDF viewer.

Hardening

  • Limit the possibility to execute code outside of the zathura SELinux domain
  • Deny launching new interactive shells from zathura

Unauthorized network connections

PDF documents may initialize outgoing network connections. Either intentionally if the user follows HTTP links in the document, or by built-in phone home functionality when the document is opened.

Hardening

  • Deny all TCP/UDP network connectivity

Unauthorized file access or modification

Zathura only requires access to a subset of files on the system:

  • The PDF files (in read-only mode)
  • Configuration file ($HOME/.config/zathura/zathurarc)
  • Bookmarks and history ($HOME.local/share/zathura/)
  • User and system fonts

In addition, zathura requires access to inter-process communication (IPC) resources.

Hardening

  • Open PDF documents in read-only mode
  • Deny access to files and directories in $HOME containing sensitive information like SSH private keys or GPG private keys

System Setup

This module is developed on Gentoo Hardened with OpenRC. (Profile: default/linux/amd64/17.1/hardened/selinux (stable))

The system is configured to run SELinux enforcing in strict mode. Thus, both network daemons and users are running confined in SELinux domains.

The system does not run systemd or PolicyKit.

zathura is installed from Gentoo Portage:

root@laptop: [~]# emerge --ask --verbose app-text/zathura app-text/poppler

What is SELinux?

Security-Enhanced Linux (SELinux) is a Mandatory Access Control (MAC) system implemented in the Linux kernel.

Traditionally, the secure state of a Linux system has been largely dependant on the behaviour of its users. The user controls access to their resources (files, processes, …) by utilizing Discretionary Access Control (DAC) mechanisms (chown/chmod/setfacl,…). The operating system then decides whether access is allowed by matching the process UID/GID against the requested resource UID/GID.

MAC complements DAC, and gives the system administrator full control over which actions that are allowed on the system. MAC is configured by policies that define which actions a subject (user, process, …) can perform on an object (files, TCP/UDP socket, …). MAC is enforced by the operating system, and a non-privileged user cannot modify the policies.

Note: MAC policies are evaluated after DAC (DAC, then MAC). If a DAC rule states that a user cannot access a file, the user is denied access to the file even if the MAC policy states otherwise.

MAC has several advantages:

  • Restrict the privileges of all users, even the superuser (root).
  • Limit the impact of a vulnerability in a network daemon (sandboxing).
  • Control access to resources at the system call level.

More information: SELinux - wiki.gentoo.org

Generate zathura policy module skeleton

The SELinux policy module template is generated using sepolicy generate:

esp0x31@laptop: [~/system/security/selinux/zathura]$ sepolicy generate --application -u staff_u -n zathura /usr/bin/zathura
Failed to retrieve rpm info for selinux-policy
Created the following files:
/home/esp0x31/system/security/selinux/zathura/zathura.te # Type Enforcement file
/home/esp0x31/system/security/selinux/zathura/zathura.if # Interface file
/home/esp0x31/system/security/selinux/zathura/zathura.fc # File Contexts file
/home/esp0x31/system/security/selinux/zathura/zathura_selinux.spec # Spec file
/home/esp0x31/system/security/selinux/zathura/zathura.sh # Setup Script
esp0x31@laptop: [~/system/security/selinux/zathura]$

NOTE: The user running zathura is mapped to the SELinux user staff_u which is mapped to the SELinux policy role staff_r. The staff_u user is initially operating within the staff_t security context (domain).

Set file contexts

Define file context to ensure correct SELinux labelling of zathura’s files and directories:

# Configuration
root@laptop: [~]# semanage fcontext -a -t zathura_rw_t "/home/esp0x31/\.config/zathura(/.*)?"

# Persistent storage directory (bookmarks and history)
root@laptop: [~]# semanage fcontext -a -t zathura_rw_t "/home/esp0x31/\.local/share/zathura(/.*)?"

The configuration settings are stored in /etc/selinux/strict/contexts/files/file_contexts.local

root@laptop: [~]# semanage fcontext -l |grep zathura
/home/esp0x31/\.config/zathura(/.*)?               all files          system_u:object_r:zathura_rw_t
/home/esp0x31/\.local/share/zathura(/.*)?          all files          system_u:object_r:zathura_rw_t
/usr/bin/zathura                                   all files          system_u:object_r:zathura_exec_t
/usr/bin/zathura                                   regular file       system_u:object_r:zathura_exec_t
root@laptop: [~]# 

As root, re-label all zathura files in $HOME, and the zathura binary:

# Relabel files in $HOME
root@laptop: [~]# restorecon -RFv /home/esp0x31/
Relabeled /home/esp0x31/.config/zathura from system_u:object_r:xdg_config_t to system_u:object_r:zathura_rw_t
Relabeled /home/esp0x31/.config/zathura/zathurarc from system_u:object_r:xdg_config_t to system_u:object_r:zathura_rw_t
Relabeled /home/esp0x31/.local/share/zathura from system_u:object_r:xdg_data_t to system_u:object_r:zathura_rw_t
Relabeled /home/esp0x31/.local/share/zathura/bookmarks from system_u:object_r:xdg_data_t to system_u:object_r:zathura_rw_t
Relabeled /home/esp0x31/.local/share/zathura/input-history from system_u:object_r:xdg_data_t to system_u:object_r:zathura_rw_t
Relabeled /home/esp0x31/.local/share/zathura/history from system_u:object_r:xdg_data_t to system_u:object_r:zathura_rw_t
root@laptop: [~/system/security/selinux/zathura]#

# Relabel the zathura binary
root@laptop: [~]# restorecon -Fv /usr/bin/zathura
Relabeled /usr/bin/zathura from system_u:object_r:bin_t to system_u:object_r:zathura_exec_t
root@laptop: [~]#

Build and install module

As root, build and install the policy module skeleton:

root@laptop: [~/system/security/selinux/zathura]# make -f /usr/share/selinux/strict/include/Makefile zathura.pp && semodule -vi zathura.pp
make: 'zathura.pp' is up to date.
Attempting to install module 'zathura.pp':
Ok: return value of 0.
Committing changes:
Ok: transaction number 59.
root@laptop: [~/system/security/selinux/zathura]#

NOTE: The policy has to be rebuilt and reinstalled after every change. On rpm-based systems, the module can be built and installed by executing zathura.sh.

SELinux domain transition

When the user executes the zathura binary, the process is transitioned into the zathura_t domain. The required rules are created when creating the policy module, and can be inspected using sesearch:

# Assign Execute permissions on zathura binary to staff_t domain
root@laptop: [~]# sesearch -s staff_t -t zathura_exec_t -c file -p execute -A
allow staff_t zathura_exec_t:file { execute getattr ioctl map open read };
root@laptop: [~]#

# The zathura binary is an Entrypoint to the zathura domain
root@laptop: [~]# sesearch -s zathura_t -t zathura_exec_t -c file -p entrypoint -A
allow zathura_t zathura_exec_t:file { entrypoint execute getattr ioctl lock map open read };
root@laptop: [~]#

# The staff_t domain has permissions to Transition into the zathura domain
root@laptop: [~]# sesearch -t zathura_t -c process -p transition -A
allow staff_t zathura_t:process transition;
root@laptop: [~]#

Customizing the SELinux policy module

Implementing a SELinux policy module is complex. Some recommendations:

  • Consult documentation at:
    • https://wiki.gentoo.org/wiki/SELinux
    • https://selinuxproject.org/
    • man 8 selinux and man 1 sesearch
  • Use audit2allow, audit2why, and strace --failed
  • Monitor the audit log for SELinux denials
  • Use pre-defined interfaces and definitions (/usr/share/selinux/strict/)

In this blog post, we focus on only a subset of the module. The complete module is located here [gitlab.com]

Overview

The policy module comprises three files; zathura.fc, zathura.if and zathura.te. The majority of the policy is defined in the type enforcement file (zathura.te). The policy defines rules for several resources:

  • Allow transition into the zathura_t domain from staff_t
  • Define explicit deny rules to mitigate threats described in threat model
  • Handle process operations, FIFO, sockets and dbus
  • Manage zathura_t resources (zathura_rw_t)
  • Restrict access to $HOME folder and files
  • Allow access to Wayland, tty/pts, themes (GTK and cursor) and Fonts (user and system)
  • Manage access to temporary file systems

Explicit deny operations

An important section of the policy is the rules that denies access to resources. This section utilizes both pre-defined functions (rw_socket_perms) and directly defines system calls (read and open):

################################
#
# Explicit SELinux deny rules
#

# Read-only access to files in $HOME
neverallow zathura_t user_home_t:file { write create append link execute};

# Deny TCP socket read/write operations
neverallow zathura_t self:tcp_socket { rw_socket_perms };

# Deny UDP socket create operations
neverallow zathura_t self:udp_socket create;

# Deny read from $HOME/.ssh
neverallow zathura_t ssh_home_t:dir read;

# Deny read from $HOME/.gnupg
neverallow zathura_t gpg_secret_t:dir read;

# Deny launch of new shell
neverallow zathura_t shell_exec_t:file open;

Granular control of files in $HOME

Access to files in $HOME is restricted using system calls and interfaces (xdg_read_config_files):

###############################
#
# $HOME folder and files
# Restrict access to user_home_t files
#

# Access to $HOME folder
allow zathura_t home_root_t:dir { getattr search };

# Hardened ($HOME folder).  Read-only access to **user_home_t** files.
allow zathura_t user_home_dir_t:dir { getattr search read open watch write };
allow zathura_t user_home_dir_t:file { getattr open read create link };

# Read access to $HOME/.config
xdg_search_config_dirs(zathura_t)
xdg_watch_config_dirs(zathura_t)
xdg_read_config_files(zathura_t)

# Read-only access to $HOME/.local
xdg_watch_data_dirs(zathura_t)
xdg_read_data_files(zathura_t)

# Read-only access to $HOME/.cache
xdg_watch_cache_dirs(zathura_t)
xdg_search_cache_dirs(zathura_t)
xdg_read_cache_files(zathura_t)

# Permit staff_t domain to open zathura files
allow staff_t zathura_t:dir search;
allow staff_t zathura_t:file { open read };

Access to dbus (IPC)

To improve readability and adhere to the DRY principle, create and use interfaces. This inteface can be called to control access to the DBUS session bus:

########################################
## <summary>
##      Send and receive messages from
##      zathura over dbus.
## </summary>
## <param name="domain">
##      <summary>
##      Domain allowed access.
##      </summary>
## </param>
#
interface(`zathura_dbus_chat',`
        gen_require(`
                type zathura_t;
                class dbus send_msg;
        ')

        allow $1 zathura_t:dbus send_msg;
        allow zathura_t $1:dbus send_msg;
')

Validate sandbox

After installing the policy module, we can verify the mitigation rules enforced by the zathura_t policy.

First, transition into the zathura_t domain by launching a shell with newrole.

esp0x31@laptop: [~]$ id -Z
staff_u:staff_r:staff_t
esp0x31@laptop: [~]$
esp0x31@laptop: [~]$ newrole -t zathura_t
Password:
bash: hostname: command not found
esp0x31@: [~]$ id -Z
staff_u:staff_r:zathura_t
esp0x31@: [~]$

Deny UDP connections

Test connecting to a network resource using UDP:

esp0x31@: [~]$ nc -z -v google.com 443
nc: getaddrinfo for host "google.com" port 443: Temporary failure in name resolution
esp0x31@: [~]$

zathura is not authorized to resolve the DNS address, SELinux creates the following entry in audit log:

Mar 08 18:34:17 laptop kernel: audit: type=1400 audit(1645860857.586:7135): avc: denied { read } for pid=15119 comm=”nc” name=”hosts” dev=”dm-1” ino=131301 scontext=staff_u:staff_r:zathura_t tcontext=system_u:object_r:net_conf_t tclass=file permissive=0

Test connecting to an IP address over UDP:

esp0x31@: [~]$ nc -z -v -u 8.8.8.8 53
esp0x31@: [~]$ echo $?
1
esp0x31@: [~]$

Zathura is not allowed to create an UDP socket:

Mar 08 18:35:10 laptop kernel: audit: type=1400 audit(1645860857.586:7136): avc: denied { create } for pid=15119 comm=”nc” scontext=staff_u:staff_r:zathura_t tcontext=staff_u:staff_r:zathura_t tclass=udp_socket permissive=0

Deny TCP connections

Test connecting to a network resource using TCP:

esp0x31@: [~]$ nc -z -v 142.250.74.46 443
esp0x31@: [~]$ echo $?
1
esp0x31@: [~]$

zathura is not allowed to create a TCP socket:

Mar 08 18:36:09 laptop kernel: audit: type=1400 audit(1645860969.743:7137): avc: denied { create } for pid=15132 comm=”nc” scontext=staff_u:staff_r:zathura_t tcontext=staff_u:staff_r:zathura_t tclass=tcp_socket permissive=0

Restrict access to SSH keys

Test accessing SSH private keys:

esp0x31@: [~]$ ls -la .ssh
ls: cannot access '.ssh': Permission denied
esp0x31@: [~]$

zathura is not allowed to do any file or directory operations on $HOME/.ssh:

Mar 08 18:39:38 laptop kernel: audit: type=1400 audit(1645861178.260:7183): avc: denied { getattr } for pid=23637 comm=”ls” path=”/home/esp0x31/.ssh” dev=”dm-3” ino=8258997 scontext=staff_u:staff_r:zathura_t tcontext=staff_u:object_r:ssh_home_t tclass=dir permissive=0

Modifying PDF documents

Launch the zathura application, open a PDF document, and overwrite the document:

# Open document in zathura
esp0x31@laptop: [~]$ zathura Linux_Programming_Interface.pdf

# Overwrite PDF document
:write! Linux_Programming_Interface.pdf

Writing the document fails since zathura cannot execute the write syscall.

Mar 08 20:32:34 laptop kernel: audit: type=1400 audit(1646058754.677:9627): avc: denied { write } for pid=7453 comm=”zathura” name=”Linux_Programming_Interface.pdf” dev=”dm-3” ino=5767287 scontext=staff_u:staff_r:zathura_t tcontext=staff_u:object_r:user_home_t tclass=file permissive=0

NOTE: The zathura application can communicate with other processes using dbus. This could make it possible for zathura to indirectly access resources outside of the sandbox.

Additional hardening: seccomp

Seccomp is a security mechanism in the Linux kernel which restricts the system calls that are permitted from a user mode process. zathura implements seccomp filters.

Verify seccomp status on zathura process:

# Find zathura PID
root@laptop: [~]# ps auxZ |grep zathura
staff_u:staff_r:zathura_t       esp0x31  26214  0.5  0.4 555884 101060 pts/1   Sl+  08:56   0:00 zathura
root@laptop: [~]#

# Print process details
root@laptop: [~]# grep CapInh -A 8 /proc/26214/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
NoNewPrivs: 1
Seccomp:    2
Seccomp_filters:    1
Speculation_Store_Bypass:   thread force mitigated
root@laptop: [~]# 

Note: Using ps with the Z option show that the zathura is running in the zatura_t domain.

The Seccomp_filters: 1 value indicates that zathura is using a user supplied filter [github.com]

  ...
  ALLOW_RULE(recvfrom);
  ALLOW_RULE(recvmsg);
  ALLOW_RULE(restart_syscall);
  ALLOW_RULE(rt_sigaction);
  ALLOW_RULE(rt_sigprocmask);
  ALLOW_RULE(sendmsg);
  ALLOW_RULE(sendto);
  ALLOW_RULE(write);  /* specified below (zathura needs to write files)*/
  ALLOW_RULE(writev);
  ...

Compared to SELinux, Seccomp filters are less granular. A system call executed by the process is either allowed or not, on all objects. Nevertheless, seccomp adds an additional layer of security, and is commonly used to isolate containers.

Conclusion

This blog post has discussed why and how to implement a custom SELinux policy module for zathura.

SELinux is complex. The policies operates on a system call level, and it has a steep learning curve. Nevertheless, it is an excellent tool for hardening a Linux workstation, or server.

An alternative to writing a SELinux policy module could be Bubblewrap [wiki.archlinux.org]. A lightweight sandbox application using cgroups, namespaces and seccomp.

Stay tuned for another blog post.

Updated: