Pwn2Own is an industry-level security competition organized annually by Trend Micro’s Zero Day Initiative. Pwn2Own invites top security researchers to showcase zero-day exploits against high-value software targets such as premiere web browsers, operating systems, and virtualization solutions.
We were interested in participating for the first time this year, choosing to target Apple Safari on macOS because the software & platform was one that we had not worked with before.
For the purpose of this competition, we discovered and exploited two previously unknown vulnerabilities in Apple software to achieve remote code execution as root through a single click in the Safari Web Browser.
Pwn2Own challenged us to evaluate these high profile targets in a very public manner. As an extension of our participation in this event, we would like to share a series of blogposts that detail a formulaic approach towards breaking an unfamiliar target.
Each post will provide a candid look at critical points of the exploit-development lifecycle:
- Overview, target selection, discovered vulnerabilities (this post)
- Performing root-cause analysis of a JSC vulnerability
- Weaponizing a JSC vulnerability for single-click RCE
- Evaluating the Safari sandbox, and fuzzing WindowServer on MacOS
- Weaponizing a Safari sandbox escape
Web browsers are the portal through which most users explore the broader web. As our expectations for the web have evolved, browsers have took on immense levels of complexity in an effort to stay relevant. It is inevitable that software at this scale will contain bugs, some of these issues being security critical.
This model is hard for browser vendors to constrain without regressing the web that we know and use today. As attackers, it is our goal to break out of this limited execution environment.
JSC: Vulnerability Discovery and Exploitation
For the purpose of Pwn2Own we were interested in burning bugs that we could discover relatively quickly, ones that would likely be bound by a shorter lifetime. We built a distributed fuzzing harness augmented by a few open source projects to create a simple coverage-guided, grammar based JS fuzzer.
After roughly two weeks of fuzzing, evaluating coverage, improving JS grammars, and triaging less interesting crashes, our fuzzer produced a testcase with a particularly intriguing backtrace:
Root-cause analysis on a minimized version of this testcase revealed what was actually happening: We had discovered a race condition between
array.reverse() and Riptide, the new, concurrent garbage collector for JSC.
Under the right circumstances, a well timed call to
array.reverse() would produce a JSArray containing a number of freed objects scattered throughout. Reliably winning the race gave us a unique & powerful primitive: Arbitrary UAF of any JS object.
In part two of this series, we will detail the construction of our JS fuzzer as a means to vulnerability discovery, and part three will describe our use of record-replay debugging technology to root-cause this tricky race condition.
Part four will provide the PoC we built to consistently win this data race, and how we leveraged this vulnerability to achieve remote code execution (RCE) in the context of Safari.
Sandbox Escape: WindowServer
To better protect their users, respectable web browsers will employ sandboxing technology in an effort to isolate themselves from the rest of the system. In software security, sandboxes are used to limit the extent of damage that an attacker can perform to a system in the event of a total application compromise.
Reviewing public research of Safari’s sandbox brought our attention to the macOS WindowServer. The WindowServer is a userspace system service responsible for drawing and managing various graphical components of macOS.
Fundamentally, the WindowServer works by processing mach_messages received from applications running throughout the system. There are roughly 600 endpoints in the WindowServer that act as handlers for these messages. These handlers highlight privileged attack surface that can be reached from within the sandbox.
WindowServer looked like an ideal target: it runs as root, lives in userspace (easier to debug / evaluate), has a large amount of attack surface, and a notable history of exploitable security issues (1,2,3, …).
WindowServer: Vulnerability Discovery and Exploitation
The WindowServer is an undocumented private framework. It is not meant to be interfaced with directly and is wrapped by various higher level public graphics libraries. Limited by what time we had to evaluate the some 600 undocumented endpoints, we instead turned towards building a (simple) in-process fuzzer.
Within WindowServer, we had identified three distinct dispatch routines where all incoming mach_messages must travel through before being ferried to their explicit handler functions.
With the help of Frida, we hooked these dispatch routines so we could examine, record, bitflip, and replay messages as they passed through the WindowServer. Bitflipping messages that normal applications generated allowed us to rapidly evaluate (eg, fuzz) large chunks of attack surface with only a naive understanding of the underlying subsystem.
Running our fuzzer for less than 24 hours on the MacBook we purchased for Pwn2Own produced a crashing Out-of-Bounds read. More importantly, this crashing callstack was only one call deep from a WindowServer endpoint. Finding such a “shallow” crash was ideal because it implied the bug would be relatively easy for us to trigger and control.
Replaying our recorded bitflips, we were able to reliably reproduce and subsequently root cause the crash:
The crash can be attributed to a classic signed comparison issue.
_CGXRegisterForKey(...) takes an attacker controlled array
index which it expects to be an integer from zero to six. However, this check was implemented as a signed operation. Passing in a negative
index (eg, -10000) would incorrectly bypass this check and index the array out of bounds.
This was fixed as CVE-2018-4193:
Interestingly enough, we collided on this bug with Richard Zhu, a fellow competitor at Pwn2Own. Ultimately, both Richard and ourselves failed to independently land this bug in the three attempts given at Pwn2Own. Certain constraints and some very unfortunate coincidences made this vulnerability rather difficult to exploit.
In the sixth and final blogpost of this series, we will discuss the complexities of the CVE-2018-4193, and the degree of code gymnastics it took to weaponize a sandbox-escape around this vulnerability.
Pwn2Own is one of the few venues where the broader public can glimpse the enigmatic tradecraft of zero-day development. With this series of blogposts, we hope to demystify parts of an otherwise opaque process, learned through expensive mistakes and sheer dedication.
Next week, we’ll expand upon our JSC fuzzing efforts and walk through the process of performing root cause analysis on a complex race condition using advanced debugging techniques & technologies.
Update: The language in this post has been updated to reflect the series as having six blogposts, instead of five.