Bypass macOS Rootless by Sandboxing

This bug has been fixed in Mojave Beta, but still present in latest High Sierra (10.13.5). It’s a logic bug that an entitled binary tries to load an insecure external library controllable by environment variable. To exploit it we need to abuse sandbox, which is interesting that sometimes a mitigation could be turned to an exploit.

CoreSymbolication(/System/Library/PrivateFrameworks/CoreSymbolication.framework) has some private api for symbolication. When demangling swift application symbols, it tries to load external library in following order:

  • /System/Library/PrivateFrameworks/Swift/libswiftDemangle.dylib
  • /Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libswiftDemangle.dylib
  • /usr/lib/libswiftDemangle.dylib
  • ${xcselect_get_developer_dir_path()}/Toolchains/XcodeDefault.xctoolchain/usr/lib/libswiftDemangle.dylib
handle = _dlopen("/System/Library/PrivateFrameworks/Swift/libswiftDemangle.dylib", 1);

if (!handle && ((len = get_path_relative_to_framework_contents("../../Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/", "libswiftDemangle.dylib", alternative_path, 0x400), len == 0 || (handle = _dlopen(alternative_path, 1), handle == 0)))) && ((len2 = get_path_relative_to_framework_contents("../../usr/lib/libswiftDemangle.dylib", alternative_path, 0x400), len2 == 0 || (handle = _dlopen(alternative_path, 1), handle == 0)))) {
  handle_xcselect = _dlopen("/usr/lib/libxcselect.dylib", 1);
  if (handle_xcselect == 0)
    goto cleanup;

  p_get_dev_dir_path = (undefined *)_dlsym(handle_xcselect, "xcselect_get_developer_dir_path");

  if ((p_get_dev_dir_path == (undefined *)0x0) || (cVar2 = (*(code *)p_get_dev_dir_path)(alternative_path, 0x400, &local_42b, &local_42a, &local_429), cVar2 == 0)) {
    handle = 0;
  } else {
    _strlcat(alternative_path, "/Toolchains/XcodeDefault.xctoolchain/usr/lib/libswiftDemangle.dylib", 0x400);
    handle = _dlopen(alternative_path, 1);


  if (handle == 0)
    goto cleanup;

__ZL25demanglerLibraryFunctions.0 = _dlsym(handle, "swift_demangle_getSimplifiedDemangledName");

The function xcselect_get_developer_dir_path will return environ variable DEVELOPER_DIR if it’s set. Absolutely controllable.

Actually the first libswiftDemangle.dylib candidate does exist. Will it reach the vulnerable branch? I’ll talk about it later.

Apple has built-in in:


And they are entitled:

➜ ~ jtool --ent `which symbols`
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "<>">
<plist version="1.0">

With this entitlement, SamplingTools can attach to SIP protected process with task_for_pid, even without root privilege.

$ vmmap Finder | head -n 8
Process: Finder [42164]
Path: /System/Library/CoreServices/
Load Address: 0x10515f000
Version: 10.13.5 (10.13.5)
Build Info: Finder_FE-1054005004000000~3
Code Type: X86-64
Parent Process: ??? [1]

LLDB fails even it’s root:

$ sudo lldb -n Finder
(lldb) process attach --name "Finder"
error: attach failed: cannot attach to process due to System Integrity Protection

So this seems to be like a meta entitlement. With it you can just inject to other entitled process and gain arbitrary entitlement.

Let’s just start an application compiled with swift, then run symbols [pid] -printDemangling and it will call CoreSymbolication!demangle, which has potential ability to load insecure code.

But there are two problems. First, the last branch looks impossible to be reached because /System/Library/PrivateFrameworks/Swift/libswiftDemangle.dylib exists.

Actually we can just block them with a sandbox. Yeah, use the security facility to trigger something insecure.

(version 1)
(allow default)
(deny file-read*
  (literal "/System/Library/PrivateFrameworks/Swift/libswiftDemangle.dylib")
  (literal "/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libswiftDemangle.dylib")
  (literal "/usr/lib/libswiftDemangle.dylib")

Then, spawn a child process with symbols [pid] -printDemangling to trigger dylib hijack.

Now we have a second problem. It crashes.

System Integrity Protection: enabled
Crashed Thread: 0 Dispatch queue: Type: EXC_BAD_ACCESS (Code Signature Invalid)
Exception Codes: 0x0000000000000032, 0x000000010d745000
Termination Reason: Namespace CODESIGNING, Code 0x2
kernel messages:
External Modification Warnings:
Process used task_for_pid().
VM Regions Near 0x10d745000:
 MALLOC_LARGE 000000010d70a000-000000010d745000 [ 236K] rw-/rwx SM=PRV
--> mapped file 000000010d745000-000000010d746000 [ 4K] r-x/r-x SM=PRV Object_id=2929ab85
 mapped file 000000010d748000-000000010d762000 [ 104K] r--/r-- SM=ALI Object_id=2af85085Application Specific Information:
dyld: in dlopen()
12 libdyld.dylib 0x00007fff66c9fd86 dlopen + 86
13 0x00007fff52d15332 invocation function for block in call_external_demangle(char const*) + 348
14 libdispatch.dylib 0x00007fff66c64e08 _dispatch_client_callout + 8
15 libdispatch.dylib 0x00007fff66c64dbb dispatch_once_f + 41
16 0x00007fff52cb880f demangle + 298
17 0x00007fff52cb85e3 TRawSymbol<Pointer64>::name() + 75
18 0x00007fff52cbd88e CSSymbolGetName + 166 in latest macOS are code signed with Library Validation flag, so loading unsigned dylib is prohibited.

➜ ~ codesign -dvvv `which symbols`
Format=Mach-O thin (x86_64)
CodeDirectory v=20100 size=1384 flags=**0x2000(library-validation) **hashes=36+5 location=embedded

I just happened to have an El Capitan virtual machine and I looked into it. The previous SamplingTools distribution has valid code signature, but no library validation flag. So just copy to High Sierra and it works.

Although we can now invoke task_for_pid on any restricted process, it still requires the same euid, which means we need a local root privilege escalation exploit as part of the chain.

Now inject into diskmanagementd and you’ll have the entitlement, which means the privilege to modify /System and spawn a shell without rootless restriction.

The bug has been fixed in Mojave Beta, no more external library, finally.

void ____ZL22call_external_demanglePKc_block_invoke(void) {
  char *bDoNotDemangleSwift;
  void *handle; 

  bDoNotDemangleSwift = _getenv("CS_DO_NOT_DEMANGLE_SWIFT");
  if ((bDoNotDemangleSwift == NULL) ||
     (((byte)(*bDoNotDemangleSwift - 0x30U) < 0x3f &&
      ((0x4000000040000001U >> ((ulong)(byte)(*bDoNotDemangleSwift - 0x30U) & 0x1f) & 1) != 0)))) {

    handle = _dlopen("/System/Library/PrivateFrameworks/Swift/libswiftDemangle.dylib",1);
    if (handle != 0) {
      __ZL25demanglerLibraryFunctions.0 = _dlsym(handle,"swift_demangle_getSimplifiedDemangledName");

Update 2019-05-14

This bug be exploited for kernel privilege escalation. Please refer to the slides for my HITB Ams 2019 talk:

It’s not XNU who validates code signature for kernel extensions, but those userspace executables that own the entitlement entitlement. These binaries are kextd, kextutil and kextload on 10.13.x.

Once you own the entitlement, you rule the kernel. The process can invoke kext_request to kindly ask XNU to load an extension:

kern_return_t kext_request(
  host_priv_t host_priv,
  uint32_t user_log_flags,
  vm_offset_t request_data,
  mach_msg_type_number_t request_dataCnt,
  vm_offset_t *response_data,
  mach_msg_type_number_t *response_dataCnt,
  vm_offset_t *log_data,
  mach_msg_type_number_t *log_dataCnt,
  kern_return_t *op_result);

Parameter request_data is an MKEXT message, serialized in XML format, while response_data is for reading the response back and log_data gives the logs.

This is an example of MKEXT request:


It consists of three parts:

  • header
  • file entry
  • plist

structure of the header

The header defines basic information like packet length, checksum, version and CPU type. File entry has the full binary of the kernel extension. A single MKEXT request can have multiple file entries.

typedef struct mkext2_file_entry {
  uint32_t  compressed_size;  // if zero, file is not compressed​
  uint32_t  full_size;        // full size of data w/o this struct​
  uint8_t   data[0];          // data is inline to this struct​
} mkext2_file_entry;

At the end of the packet is the plist metadata. It has the identifier, dependencies, path and part of the Info.plist of the kext bundle.

Since it’s the userspace that does the validation, my exploit simply patches the kill-switch of process kextd to allow arbitrary unsigned kext to be loaded.

The service kextd checks the following conditions when we run kextload:

  • Signed: OSKextIsAuthentic
  • To avoid malicious modification during kext loading, it has a special staging process that the extension must be moved to a SIP-protected location. This process is ensured by function rootless_check_trusted_class​
  • Finally, kextd will ask user’s approval by invoking this method -[SPKernelExtensionPolicy canLoadKernelExtensionInCache:error]​

All of the functions have a same shortcut that, when csr_check (the syscall that checks the state of SIP) returns false, it will load arbitrary kext and ignore all the requirements. By reusing kextool, we don’t have to manually serialize a valid kext_request on our own.

Here’s the exploit to load an unsigned kernel extension on macOS 10.13: