The mysterious case of (deny dynamic-code-generation)

My day job is working on sandboxing for Firefox. In the context of a browser, sandboxing refers to the processes that run web pages, generally called “content” or “renderer” processes. These are in contrast to the “parent” or “browser” process, which coordinates the content processes and is not sandboxed, so it can do things like write files anywhere on disk to save downloaded files or access the camera.

A related computer security technique is exploit mitigation. In the context of a browser specifically memory corruption exploit mitigation. Exploit mitigation is a family of approaches with the goal of making it harder for an attacker to take a memory corruption vulnerability (heap or stack buffer overflow, use after free, type confusion, etc.) and turn that vulnerability into full arbitrary code execution. Examples of exploit mitigation are ASLR and stack cookies. Justin Schuh, of the Chrome Security Team, has an excellent blog post comparing Chrome and Edge’s sandboxing vs. exploit mitigation approaches.

A few years ago, Microsoft added a new exploit mitigation called Arbitrary Code Guard (ACG). In short, ACG makes mmap and mprotect (I’m going to use the POSIX names throughout this post, instead of mixing the Windows and POSIX names) refuse to allow new pages to be created with PROT_EXEC, or existing PROT_EXEC pages to be marked PROT_WRITE. In unsandboxed processes, it’s common to achieve remote code execution by hijacking control flow to call system or a similar function which gives the attacker a fresh process of their choosing. However, inside a browser sandbox an attacker can’t create new processes; instead they need to run their payload inside the process’s address space. One technique for doing this is to hijack control flow to create a new page of executable memory, place shellcode inside that memory, and then hijack control flow to jump to the shellcode. ACG mitigates this technique, by preventing the attacker from creating a page of executable memory with their shellcode; instead they are forced to use other techniques such as ROP to encode their payload.

One of my recent projects for Firefox was investigating whether enabling ACG made sense for us. Enabling ACG in a browser is a complex task, because JavaScript JIT compilers rely on allocating new executable memory pages at runtime; Edge is the only browser which currently leverages ACG. However, that’s for another blog post. Instead the rest of this post is about my investigation into whether it was possible for us to have a mitigation like ACG on macOS.

Folks familiar with the iOS security model probably recognize that on iOS all processes have an ACG like mechanism – besides Safari, no process is allowed to allocate new executable pages at runtime, because of the requirement that all code on iOS has to be signed – however, it wasn’t clear if macOS has a similar mechanism. As a bit of a digression, macOS has two ways to sandbox your process. One is to use macOS’s App Sandbox API, which is the official, supported, and documented way to sandbox your application. At present, no browser (Chrome, Firefox, or Safari) uses this API. Instead they all use the private macOS “Seatbelt” API. The Seatbelt API is really a subset of Scheme that provides a declarative syntax for expressing what permissions a process should have. It is by far my favorite sandboxing API of any I have worked with (primarily Windows and Linux’s), the one downside is that there’s no documentation and anytime you file a bug Apple tells you that you should use App Sandboxing instead. Oh well.

Given there’s no documentation, there’s two tactics we use to figure out what functionality is available: 1) disassembling the binaries that provide the functionality, 2) looking at the sandbox policies that macOS ships for system binaries. So when I went to see if macOS had anything like ACG, I started grepping around with the system profiles in /System/Library/Sandbox/Profiles/. Eventually I stumbled upon (deny dynamic-code-generation) in a few of the profiles. Off the cuff this sounded like what I was looking for, and seemed like the sort of name that might describe iOS’s functionality. In this process I also discovered the file-map-executable policy rule, which I’ll discuss later.

So I put together a pretty quick proof-of-concept to test whether this works. All it did was apply the sandbox policy, mmap some memory, stick a simple assembly function into it, mprotect the memory as PROT_EXEC, and attempt to call it:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/mman.h>

#include <sandbox.h>

static const char kSandboxPolicy[] = R"(
(version 1)
(deny default)
(deny dynamic-code-generation)
)";

void enable_sandbox() {
  char *err;
  int rv = sandbox_init(kSandboxPolicy, 0, &err);
  if (rv) {
    fprintf(stderr, "sandbox_init error: %s\n", err);
    exit(1);
  }
}

static const uint8_t kShellcode[] = {
  // movq %rdi, %rax
  0x48,
  0x89,
  0xf8,
  // addq $4, %rax
  0x48,
  0x83,
  0xc0,
  0x04,
  // ret
  0xc3,
};

typedef uint64_t (*Function)(uint64_t);

Function create_function(const uint8_t *shellcode, size_t length) {
  void *codebuf = mmap(nullptr, length, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  assert(codebuf != nullptr);
  memcpy(codebuf, kShellcode, length);
  int rv = mprotect(codebuf, length, PROT_READ | PROT_EXEC);
  assert(rv == 0);

  return (Function)(codebuf);
}

int main() {
  enable_sandbox();
  printf("Sandbox enabled!\n");

  Function f = create_function(kShellcode, sizeof(kShellcode));
  printf("Function created!\n");

  uint64_t res = f(12);
  printf("Function called! 12 + 4 = %llu\n", res);
}

Compile it, run it, and… it works… The program runs without error, rather than failing to mprotect the memory PROT_EXEC or crashing as I would have expected. I spent some time seeing if various changes would give me the behaviour I expected: mapping the memory PROT_READ | PROT_WRITE | PROT_EXEC instead of just PROT_READ | PROT_EXEC, including PROT_EXEC in the mmap rather than mprotect, mmaping multiple pages instead of a single one. But I struck out, none of these got me what I needed: an exploit mitigation that protected against an attacker creating new executable pages.

At this point I decided that I should report this to Apple as a potential security issue. I was a bit on the fence, since I couldn’t know for certain what the intended behaviour of dynamic-code-generation was without documentation, and maybe it was never expected to work at all on macOS! Nonetheless, there were a few macOS sandbox profiles that were using it which was evidence that someone expected it to work on macOS. Plus I had a clear reproducer, so if this was expected behaviour it should be easy enough for them to recognize it as such.

Unfortunately this story has an unhappy ending. Apple declared that my PoC: “did not demonstrate any behavior from dynamic-code-generation that was unexpected.” I’m still not sure what the expected behaviour is! Perhaps someone who is better at reverse-engineering than I am will read this and figure it out.

If (deny dynamic-code-generation) had done what I’d expected, it’d have been the missing piece in building an ACG-alike mitigation for macOS. The other piece, which did exist, was limiting what sorts of dynamic libraries can be loaded. On Windows, this is achieved with Code Integrity Guard, which requires that any DLLs which are loaded be signed. On macOS we achieve this with the file-map-executable permission. By default, macOS’s sandbox policy allows loading a dynamic library from anywhere you can read files from. With file-map-executable you can add a deny-all rule and then whitelist particular places on disk to load libraries from. We’ve now landed a patch for Firefox which limits us to loading libraries only from system directories and from the Firefox.app directory – content processes can’t write to any of those directories, meaning that they can’t load any attacker controlled dynamic libraries.

I’m hopeful Apple will consider providing an ACG-like mitigation for macOS, as they do on iOS. In the meantime, hopefully this blog post serves as a useful resource for other folks exploring sandboxing and exploit mitigation on macOS.