Merry HaXmas! The holidays are yeeting toward us, the decade is drawing to a close, and there are few days left to finish your 2010s haxlist. Last year, I wrote about a few nifty ways to stealthily stage and execute payloads on Linux. One downside was that the coolest, stealthiest method, which maps and runs a process much like the
execve syscall but in userspace, was easily mitigated by tools like the SELinux's commonly-applied
execmem permission. Since then, I've had an inkling of a way to bypass this, and all I wanted for HaXmas this year was to get it out of my system.
The SE is for Super-Esoteric
Distros and phone manufacturers have been using SELinux to foil hackers and rooters for a while now, but providing a performant, fine-grained permission framework for system resources is a tricky task and the policy language can be a bit opaque with all the rules and macros that tend to end up in the final product. Below is an overview of some important concepts needed to understand how and where memory laundering can be applied. For a more in-depth guide, check out the wiki or your favorite distro's documentation.
Growing out of the NSA's work to add Mandatory Access Control to Linux, SELinux provides ways to specify sets of restrictions, or "policies", that only the policy administrator (not necessarily any root session) can change. These restrictions are scoped across different groups of files and processes, termed "domains", types of resources, called "object types", "users", and "roles".
For example, a web server will be unable to serve files belonging to a domain that an enforced policy does not explicitly grant the server read access to, even files the web server's user has been given permission to read using normal filesystem permissions, like
/etc/passwd, which is generally world-readable. Additionally, non-system processes may be restricted from transitioning to the web server's domain, protecting the contents of the files the web server does use. Files are tagged via filesystem extended attributes with defaults set by the policy. File security contexts can be viewed with
ls -Z, and process contexts can be viewed with
ps -Z. New files are labeled according to the policy context rules, and processes either inherit their parent's context or transition to the domain of the executable, if allowed. Objects not matching a specific rule are normally placed in the
Modern policies are modular, allowing fine-grained restrictions and overrides to be added without recompiling the whole policy. Policy variables, called "tunables" or "booleans", are available to turn on or off restrictions at runtime that may be good ideas, but also may disrupt legitimate features of a service. The enforcement mode controls how the policy is used on a system. "Enforcing" is when restrictions are actually used to deny actions, and "Permissive" allows all activity to proceed, but actions that would be denied are logged. The enforcement mode can also be set per-domain, allowing, for example, broad system protection while tuning controls for a critical service or vice versa.
SELinux policy implementation is variable across distros and environments, but the policies tend to fall into two categories: targeted and broad. Targeted policies, including Fedora's default policy, have restrictions that are specific per-program to prevent abnormal behavior and have an almost completely unrestricted unconfined domain. Debian's default policy is more broad in scope and more closely follows the reference policy maintained by the SELinux project. It tends to feature exceptions for specific programs from rules that affect even the unconfined domain alongside targeted rules for networked and other critical services.
Keeping the memory clean
The technique I wrote about last HaXmas turned out to need only a minor tweak to work around the memory restrictions used by SELinux and similar hardening approaches. The previous approach mimics the memory mapping pattern of the common runtime linkers, only substituting anonymous memory for file-backed memory. Instead, we can create an anonymous memory-backed file, write the segment we need to execute to it, then map the anonymous file into the process image. This ensures that the kernel thinks that the underlying memory has not been modified and bypasses the dirty-memory execution restrictions. This approach from last year uses a similar pattern, but with DLLs instead of full executables. You can see the detailed changes in the PR, but the technique boils down to:
memfd = syscall(SYS_memfd_create, "", 0); ftruncate(memfd, PAGE_CEIL(segment_memory_size)); write(memfd, source_buf, file_len); mmap((void *)PAGE_FLOOR(dest), PAGE_CEIL(segment_memory_size), PROT_READ | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, memfd, 0);
While developing this, I discovered that while the filesystem used by
memfd_create(2) is immune from having its mounting flags altered and the files on it are always executable, it is affected by SELinux's available rules governing all
tmpfs files. So, how useful is the approach on hardened systems?
The memory dress code
To see which approach made the most sense, I loaded up two distros with some of the most complete SELinux coverage and found in servers and desktops across the spectrum. Fedora 31 Server using the MLS policy represented the Red Hat family, and Debian 10.2 (Buster) using its default policy represented its distro family. Debian does ship an MLS policy, but it is essentially just the Red Hat one and I wanted to see how the two flavors stacked up. I used
runcon(1) to run the proof-of-concept for simplicity when testing the restrictions in place for system service domains. Most of these domains and the programs running inside of them have several layers of protection, so even being able to use this approach in the wild would require a return-to-libc-style attack for many services. In such a scenario, reflective loading minimizes the amount of code that needs to be written and run in that style, lowering the total effort for an exploit chain.
The good news for defenders is that a lot of tmpfs execution is locked down pretty well for common network services. Both Debian and Fedora follow the SELinux reference policy in blocking a lot of execution of any form by services like web servers and
sshd. In all three policy implementations, only the
httpd_t domain has a built-in tunable variable for tweaking this, so determining which domains are protected requires a policy source deep-dive or running a proof-of-concept test. Some domains, like those used by NetworkManager and Samba, are not even allowed to write to
memfd_create(2) file descriptors, which is a nice example of least-privilege.
The good news for red teamers is that no SELinux implementation I could find completely shuts down execution of tmpfs files by default or is even capable of doing so globally without a full policy rebuild. Fedora's targeted-policy approach has even dropped the global memory execution protection tunable found in the reference policy and Debian (where the protection is disabled by default). Since
runC adopted the memfd_create-fexecve pattern to fix a vulnerability involving the magical symlink at
/proc/self/exe, using anonymous memory files to build executables is unlikely to be patched out in a meaningful way in the near future. Running executables from
/tmp, which is commonly also a memory-backed filesystem, is also required for Java's native library interface and PCRE's JIT engine among others.
This can lead to some interesting environment discrepancies. Java programs not running the
httpd_t or other locked-down domain can therefore run anonymous memory files just fine, even though they cannot directly create anonymous executable memory. Other programs with JITs, like Chromium (both the main program and the sandbox), are conversely allowed to create executable memory, but not run anonymous memory files. Additionally, non-networked services in particular can have drastically different restrictions between Debian and Fedora. The
crond_t domain, in particular is locked-down in Debian, with access to neither anonymous executable temporary files or memory. In Fedora, it has access to both, presumably for compatibility with the random stuff typically stuffed in crontabs.
Additionally, few custom deployed services are likely covered by the default file context rules and placed in an appropriately locked-down domain. The
semanage-fcontext(8) tools are your friends for checking and adding local context rules. Please do not go through the pain of setting up a secure base server only to leave a side door with full system privileges. Nobody wants incident response for HaXmas. The
httpd_t domain is a secure starting point for most web-type services and has a wide selection of booleans and companion domains for adding fine-grained control for things like configuration and content without needing to open up huge holes in the policy.
The upshot is that leaving processes with permissions to execute tmpfs files on Linux is equivalent to allowing them to execute anonymous memory and, in this author's opinion, should be locked down with the same zeal. The modification to libreflect to back executable memory with an anonymous temporary file is PR'd to the Mettle repository, be sure to check it out if you have questions or suggestions! You can also join Metasploit on Slack if that's your jam. Thanks, and Merry HaXmas!