berkesrc
  • Projects
  • Experience
  • Contact
  • Articles
entr
© 2026 Berke SerçeBack to top ↑
← All articles

Oct 25, 202610 min read

Escaping Safari's sandbox through the WindowServer

How RET2 Systems used CVE-2018-4193 in macOS WindowServer to escape Safari's sandbox and execute code as root at Pwn2Own 2018.

  • Security
  • macOS
  • Pwn2Own
  • Exploit

Executing code inside a browser does not necessarily give an attacker control over the operating system. Even after compromising JavaScriptCore, an attacker still has to escape Safari's sandbox. At Pwn2Own 2018, RET2 Systems used a memory-corruption bug in macOS WindowServer for this second stage. WindowServer manages the desktop display and, on the version they targeted, ran as root. The team had discovered the bug using a fuzzer that ran inside the service itself.

This article revisits the research described in the final post of RET2's Pwn2Own series. The vulnerability, CVE-2018-4193, allowed privilege escalation through WindowServer on macOS 10.13.3. Its root cause was a signed/unsigned comparison error in _CGXRegisterForKey(), one of WindowServer's Mach message handlers. All images below are from RET2's original post.

The team's standalone demonstration shows an unprivileged process gaining root privileges in about 90 seconds, without involving a browser.

Animated demonstration of privilege escalation through WindowServer on macOS 10.13.3
The standalone WindowServer exploit gains root privileges in about 90 seconds. Image: RET2 Systems.

A bug triggered by one Mach message

WindowServer exposes private interfaces through Mach IPC. A user process can send a message to the service by calling SLPSRegisterForKeyOnConnection() from the SkyLight framework. On the server, the message passes through _XRegisterForKey() to the _CGXRegisterForKey() handler.

An incorrect comparison in that handler allows an attacker-supplied index to access memory outside an array. Initially, this produces an out-of-bounds read. Under the right memory conditions, the same path can also perform an out-of-bounds write.

Diagram of Mach IPC communication between Safari and WindowServer
Safari communicates with WindowServer through Mach IPC. Image: RET2 Systems.

Once a WindowServer connection has been established, the core of RET2's minimal proof of concept comes down to one crafted parameter and a function call:

// Minimal trigger, from RET2 Systems' public PoC for CVE-2018-4193.
uint32_t BUG = 0x80000000 | 0x41414141;
result = SLPSRegisterForKeyOnConnection(cid, &psn, BUG, 1);

Running the PoC as an unprivileged user crashes WindowServer immediately. It reproduces the out-of-bounds access in a small, repeatable example.

Three constraints on the write

The bug does not let an attacker write any value to any address. The write has three constraints:

  1. The index addresses an array of six structures, each 24 bytes long. The target offset is calculated as index * 24, limiting writes to 24-byte increments from the array's base.
  2. Memory around the target must satisfy two preconditions before execution reaches the write.
  3. The attacker has little control over the values being written. The code stores an internal heap pointer followed by a four-byte WindowServer connection ID (CID).
Pseudocode showing the two preconditions for the out-of-bounds write
Both memory preconditions must hold before the write can occur. Image: RET2 Systems.
The out-of-bounds write stores r15 and ecx using an attacker-controlled index
The attacker controls the index, but has limited control over the values in r15 and ecx. Image: RET2 Systems.

The team needed a memory layout that satisfied those preconditions and a neighboring allocation whose contents could be usefully corrupted. Meeting the conditions alone would not help if the resulting write had no useful effect on the target object.

Finding useful corruption targets

The macOS user-mode heap allocator examined in this research follows a Hoard-style design. It places no allocator metadata between allocations, so objects can sit directly next to one another. A write beyond one allocation can therefore reach the first bytes of its neighbor.

Instructions involved in the out-of-bounds write used for cross-chunk corruption
The constrained write can be used to corrupt an adjacent allocation. Image: RET2 Systems.

RET2 reused parts of its WindowServer fuzzer with Frida to investigate possible targets. By hooking malloc, realloc, and free, the team tracked live allocations, their sizes, and their call stacks. About every ten seconds, the instrumentation replaced the first four bytes of a random subset of allocations with a marker value. This simulated the cross-chunk corruption and let the team group the resulting crashes.

Objects with a pointer as their first field were particularly useful candidates. A partial overwrite might redirect that pointer into attacker-controlled data, changing how the object behaved when used later. For Objective-C objects, established techniques could turn control of such an object into code execution through objc_msgSend().

The connection ID's bit pattern prevented the team from using that approach directly. They observed that macOS Mach port numbers always had their lowest two bits set, which they attributed to a masking operation during port creation in XNU.

In the Objective-C and CoreFoundation implementation they examined, a value with its lowest bit set was interpreted as a tagged pointer. Such a value encodes object data within the pointer-sized value itself. Overwriting the low bytes of an object pointer with a CID therefore made the runtime treat it as a tagged pointer instead of dereferencing it as an ordinary object address. The direct objc_msgSend() approach was no longer available through this partial overwrite.

Mach port numbers with their lowest two bits set
The connection ID's low bits constrain how an overwritten pointer is interpreted. Image: RET2 Systems.

The team shifted its search from CoreFoundation objects to structures managed by WindowServer itself.

An Objective-C pointer interpreted as a tagged pointer after a partial overwrite
Replacing the pointer's low bytes with a CID changes how the runtime interprets it. Image: RET2 Systems.

The HotKey list

WindowServer's HotKey objects provided a suitable target. The service keeps registered keyboard shortcuts in a linked list. The first field of a HotKey object, hotkey->next, points to the previously created hotkey.

SkyLight exposes APIs for registering, modifying, and querying these objects. If a list pointer could be redirected into attacker-controlled memory, those same APIs could read and modify fields in a fake HotKey structure.

WindowServer functions for querying and modifying HotKey objects
Some of the APIs that operate on HotKey objects. Image: RET2 Systems.

The team built the exploit around five stages:

  1. Use the vulnerability to leak an address near the end of WindowServer's MALLOC_TINY heap region.
  2. Arrange for a HotKey object to follow an attacker-controlled allocation, then partially overwrite its list pointer.
  3. Place an attacker-controlled CFMutableArray in the memory targeted by the corrupted pointer.
  4. Change one bit in a CFStringRef pointer inside the array, redirecting it to a fake object and enabling code execution.
  5. Repair the corrupted state and let WindowServer continue running.

Stage 1: learning the heap layout

RET2 used CGSSetConnectionProperty() to spray approximately 500,000 CFString objects into MALLOC_TINY. Each string contained mostly zero bytes, with a single four-byte 0x400 value in the middle. That value and the surrounding zeros were arranged to satisfy the write's two preconditions.

The out-of-bounds index was then increased from a starting value such as 0xFFEDFFC0. When an attempt reached one of the prepared memory patterns, the write modified a sprayed string without crashing WindowServer:

A heap pointer leaked by writing into a prepared string allocation
The write leaves a heap pointer and a CID inside one of the sprayed strings. Image: RET2 Systems.

The team then read back each connection property with CGSCopyConnectionProperty(), looking for the [8-byte pointer][CID] pair left by the write:

  • The heap pointer identified an address near the end of MALLOC_TINY in a freshly started WindowServer process.
  • The CID confirmed that the data came from the out-of-bounds write.

This address provided a reference point for later allocations. In the allocator examined here, MALLOC_TINY expanded toward lower addresses in predictable regions of approximately 256 MB. The team could use the leaked address to calculate where to direct subsequent heap grooming.

Stage 2: redirecting a HotKey pointer

With a reference address available, the exploit filled another 1–4 GB of memory with connection-property strings. The aim was to place a HotKey object directly after an attacker-controlled string while also covering the region that its corrupted pointer would eventually reference.

The spray was split into two parts. In the first part, the exploit identified pairs of adjacent allocations and freed the second allocation in each pair by passing a NULL value to CGSSetConnectionProperty(). This left holes in the heap. Repeated calls to CGSSetHotKey() then filled those holes with HotKey objects, placing a target object immediately after an attacker-controlled string.

Selected allocations freed to make room for HotKey objects
Adjacent allocation pairs determine which blocks are freed and replaced with HotKey objects. Image: RET2 Systems.

Triggering the bug at the chosen boundary wrote past the end of the string and placed the CID in the first four bytes of the HotKey object. A hotkey->next pointer such as 0x00007fd15cfa8730 became a value like 0x00007fd100005413, pointing into the predicted memory region instead of a real HotKey object. RET2 calls the resulting structure a dangling HotKey. The second part of the spray, completed before this corruption, ensured that the destination was allocated memory.

The next step was to identify which string overlapped the fake HotKey. Calling CGSSetHotKeyEnabled() on the dangling node, whose ID initially appeared as zero, changed a single bit in one of the sprayed strings. Reading the strings back revealed which allocation had changed, locating the memory underneath the fake structure.

WindowServer heap layout after redirecting the HotKey pointer
The HotKey list pointer now refers to an attacker-controlled allocation. Image: RET2 Systems.

Stage 3: placing an array beneath the fake HotKey

The exploit now needed to place another object in the memory referenced by the fake HotKey. Its layout had to remain compatible with the HotKey APIs. Because of the CID's low bits, the fake HotKey began at an odd address. Its next field needed to stay NULL or contain a valid pointer. Relative to an eight-byte-aligned value, CGSSetHotKeyEnabled() changed bit 24.

A suitably positioned CFMutableArray containing CFStringRef entries met those constraints. Two pointer fields in the array's header allowed the fake HotKey's next field to remain NULL. One of the array's string pointers could also be positioned so the API would change the desired bit.

Byte-level overlap between the fake HotKey and the CFMutableArray
The HotKey APIs and CoreFoundation interpret the same memory as different structures. Image: RET2 Systems.

Replacing the underlying allocation also changed the bytes used as the HotKey ID. The exploit tried the twelve unknown bits through CGSGetHotKey() until it found an ID the API accepted. That call also returned several fields from the fake structure, allowing the full CFStringRef pointer value to be reconstructed.

The recovered address revealed whether the target bit was set. If it needed to be cleared, the exploit called CGSSetHotKeyEnabled() with disable. One CFStringRef entry in the array then pointed into attacker-controlled memory in MALLOC_LARGE. Large strings placed in that region contained fake Objective-C objects and a ROP chain.

Executing code and keeping the service running

The corrupted CFStringRef now pointed to a fake string object. Freeing the parent array released its members, bringing the attacker-controlled object into the established objc_msgSend() control-flow hijacking technique.

RET2 used a custom COP gadget followed by ROP and JOP chains to map a page with read, write, and execute permissions, then transfer control to shellcode. The code ran inside the root-level WindowServer process, outside Safari's sandbox.

Final exploit layout showing the heap sprays and chained corruptions
The final memory layout connects the heap sprays, overlapping objects, and single-bit change. Image: RET2 Systems.

WindowServer also had to continue running: a crash would disrupt the desktop. After gaining code execution, the shellcode cleaned up corrupted references in the connection property dictionary and the HotKey list. It then returned control to the main Mach message loop inside SLXServer, allowing the service to resume its normal work.

WindowServer's main Mach message-processing loop
After cleanup, the shellcode returns control to the main message loop. Image: RET2 Systems.

Development time and reliability

  • CVE-2018-4193 was discovered on February 16, 2018. Turning it into a working, reliable exploit took about 100 person-hours, comparable to the time the team spent on its JavaScriptCore exploit.
  • On a 2017 MacBook Pro rebooted before each attempt, the chain succeeded in roughly 85% of more than 1,000 runs.
  • Execution averaged more than 90 seconds, most of it spent preparing WindowServer's heap layout. The team named the escape Eternal Groom.
Automated test harness running the exploit chain and recording results
Automated tests measured the exploit chain's reliability on a 2017 MacBook Pro. Image: RET2 Systems.

RET2 believed that someone with a deeper understanding of WindowServer and the macOS heap could build a faster, more reliable exploit. The team offered a commercial Binary Ninja license to the first researcher who could meet its conditions: use only CVE-2018-4193 on macOS 10.13.3, finish in under ten seconds, avoid crashing WindowServer, achieve at least 90% reliability, and publish the method.

@elvanderb won the challenge and presented the solution at OffensiveCon '19. RET2's complete Pwn2Own 2018 exploit chain is also available on GitHub.