Lately, there has been a lot of discussion in the Operating System iTC around an objective SFR in the current revisions of the protection profile, entitled FPT_W^X_EXT.1. The SFR is part of a set which are intended to address the O.INTEGRITY security objective for the OSPP. While some of the discussion has been around what the actual meaning of the SFR is and what is hoped to be gained, much of it has been around whether any operating systems even support the feature as worded. This post intends to address all of these issues.
First, it is worth reading the actual text of the SFR itself: “The OS shall prevent allocation of any memory region with both write and execute permissions except for [assignment: list of exceptions].”
The accompanying application note gives us some more insight into the rationale, as well as what is actually expected: “Requesting a memory mapping with both write and execute permissions subverts the platform protection provided by DEP. If the OS provides no exceptions (such as for just-in-time compilation), then “no exceptions” should be indicated in the assignment. Full realization of this requirement requires hardware support, but this is commonly available.”
While nearly all operating systems currently support the use of the NX bit, or the equivalent on processors such as SPARC and ARM, and will correctly mark the stack as non-executable, the fact remains that this in and of itself is deemed insufficient by the PP authors.
This is probably also where some of the confusion comes in. First of all, the name of the SFR, “W^X,” introduces some confusion for the following reasons:
- W^X (write xor execute) is the name of an implementation specifically in OpenBSD. One might therefor think that OpenBSD would meet this SFR, however it does not, as I will demonstrate below.
- When you look at the testing activity, W^X does not cover all three of the test activities, which essentially break down as follows:
- W^X (write xor execute) – ensure that pages of memory can never be both writable and executable
- X!->W (execute never write) – ensure that an executable page of memory cannot be made to be writable
- W!->X (write never execute) – ensure that a writable page of memory cannot be made to be executable
The assurance activities direct the evaluator to:
- Acquire or construct a test program which attempts to allocate memory that is both writable and executable. The evaluator will run the program and confirm that it fails to allocate memory that is both writable and executable.
- Acquire or construct a test program which allocates memory that is executable and then subsequently requests additional write/modify permissions on that memory. The evaluator will run the program and confirm that at no time during the lifetime of the process is the memory both writable and executable.
- Acquire or construct a test program which allocates memory that is writable and then subsequently requests additional execute permissions on that memory. The evaluator will run the program and confirm that at no time during the lifetime of the process is the memory both writable and executable.
Obviously, these three assurance activities, as noted above, describe some things which are a little bit different from what one might think. Additionally, they describe something different than what many operating systems (even OpenBSD) have actually implemented.
The following image shows a test program I constructed running in OpenBSD 5.8 on VMWare Workstation Professional 12.1:
There are, however, two BSD variants which do pass, and both have something in common.
First, let us look at the test running on HardenedBSD:
As we can see, all three tests pass.
NetBSD is a little different:
However, we have an option here:
Both HardenedBSD and NetBSD ported the PaX method for ASLR and executable space protection. Any Linux distribution which uses the PaX hardening model (often leveraged through grsecurity) should also pass these tests. Using SELinux to set the Boolean value for deny_execmem prevents one from mapping pages of memory which are executable at all, however, when one maps a page of memory as writable and then escalates permissions via mprotect() to PROT_WRITE|PROT_EXEC, one is able to do so:
As you see, with “setsebool deny_execmem 1”, we are unable to mmap() any page that is PROT_EXEC to start, but with mprotect(), we can create a PROT_WRITE|PROT_EXEC page of memory.
So, as we can see through illustration, there are clearly operating systems which support the SFR as written. Saying that there are none, as some have suggested, is false. HardenedBSD passes all three tests out of the box, and NetBSD will do so with a single sysctl tweak. Since they are using the PaX model, anything else using PaX, such as a grsecurity-enabled Linux distribution pass these assurance activities as well.
That doesn’t mean that the SFR can’t be improved, however. For instance, it does not address dual-aliasing a memory mapped file where on map has PROT_WRITE and the other has PROT_EXEC. Attempting to do so succeeds on all operating systems tried.
So, to recap where we are:
- The SFR can, and has, been met by several operating systems, though they are primarily non-commercial.
- The SRF is, however, written essentially to the PaX model for doing executable space protection. This isn’t to say that one must do things HOW PaX implemented the protections, but in order to meet the SFR, an OS needs to implement what they did.
- OpenBSD, the OS for which the SFR is named, does not actually pass the assurance activities. The name of the SFR should be changed, since what OpenBSD W^X actually implements is not what is being asked for or tested in this SFR.
- There are still edge cases, such as dual-mmap aliasing files, which are not handled by the assurance activity, and for which no operating system appears to provide a prevention mechanism. In fact, there are documents online illustrating doing exactly this to get around potential issues related to this type of restrictions. Depending on the LOE to implement protection around this, it may or may not make sense to extend the assurance activities and have the SFR address this situation directly.
Given all of this, it makes sense to leave this as an Objective SFR for the time being. Hopefully it will serve as a nudge for OS vendors to move towards full implementation.
** UPDATE **
In the comments below, a reader helpfully brought up the new pledge(2) system call in OpenBSD 5.9 and how it might be used to edge further towards meeting the strict enforcement requirement of the protection profile. I responded to the comment below, and I will likely do another post in the future delving more into pledge(2), particularly in the context of the Software Application Protection Profile, since it is more inline with providing a mechanism for meeting requirements in that protection profile. I will likely also do a side-by-side comparsion against the similar Capsicum framework found in FreeBSD.
However, keeping in mind that, as written, the protection profile requires mandatory enforcement with the ability to opt-out on a per-application basis in order to meet needs such as those of JIT compilers, I would like to provide a few further illustrations of how pledge(2) doesn’t further compliance with this specific objective requirement in the OS protection profile.
First, a similar illustration to the one from OpenBSD 5.8, we attempt to map a page of memory as W|X, and this succeeds. Code sample and execution output are in the right terminal, with procmap output in the left:
The test tool prints out the PID and the address of *page, which contains the mmap(2)’d page of memory. In procmap, we see that we do have W|X permissions.
In this next example, we see that I use pledge(“stdio”, NULL) above the call to mmap(2), so that the peldge enforcement kicks in prior to my attempt to map W|X memory. Attempting to run the code causes it to die with sigabrt and a core dump, as by default a pledge(2) on stdio disable the ability to map memory as PROT_EXEC.
Next, however, we see that If I move the peldge(2) call below the call to mmap(2), I am successfully able to map a W|X page of memory. Again, we confirm this with procmap.
In the final example, I move the pledge(2) call back above the mmap(2) call. This time, in addition to pledging stdio, I also pledge prot_exec. Doing so allows me to now map a W|X page of memory.
So, while I am absolutely a fan of pledge(2) due to its ease of use and ability to help enforce safe, correct behavior in software, the lack of mandatory, across-the-board enforcement of W^X pages in userland without the developer having to review or modify code first in order to conform to policy, does not meet the letter of the requirement that is being discussed here. That shouldn’t be taken as a slight against OpenBSD, their implementation, or all they have done for software quality and security as well as general Internet hygiene, just an illustration that the requirement, as written, is almost expecting to be met via a PaX-model implementation.