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.

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.

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:
- 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. - Memory around the target must satisfy two preconditions before execution reaches the write.
- 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).


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.

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.

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

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.

The team built the exploit around five stages:
- Use the vulnerability to leak an address near the end of WindowServer's
MALLOC_TINYheap region. - Arrange for a HotKey object to follow an attacker-controlled allocation, then partially overwrite its list pointer.
- Place an attacker-controlled
CFMutableArrayin the memory targeted by the corrupted pointer. - Change one bit in a
CFStringRefpointer inside the array, redirecting it to a fake object and enabling code execution. - 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:

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_TINYin 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.

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.

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.

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.

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.

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.

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.