In last year’s Pwn2Own, the team at Ret2 Systems developed an interesting exploit chain for MacOS High Sierra through bugs they found in Safari and the MacOS WindowServer. A few months later, they published an excellent walkthrough of their methodology and the bugs used to compromise the machine. Of particular interest to me was their description of how they used Frida and a relatively basic fuzzing strategy to find an exploitable bug in the SkyLight library. I decided to spend some time to reimplement their methodology in Mojave as a way to get some hands on experience with Frida and mach message fuzzing.

Before we get into it, I’d like to thank the team at Ret2 Systems for their help in reproducing their research, and for the original write-up. Often we never get a chance to understand these chains, and the six-part series is a must-read for anyone interested in this kind of stuff. Follow along if you’re interested in the process, but for those who’d rather just get started fuzzing SkyLight you’ll find everything you need here.

While I would recommend reading all six posts in the Ret2 Systems Pwn2Own walkthrough, the most important for our purposes is post four, Cracking the Walls of the Safari Sandbox. In this post Ret2 describes their methodology and why they chose WindowServer, so we’ll skip that here.

intercepting messages

The first step is to identify the points in the SkyLight Library where we will be using Frida Interceptor to read and modify incoming mach messages. To do this, we first need to find the addresses of the MIG subsystems. You can get these addresses by using either jtool, by searching for the string “subsystem” in the library itself using your favorite disassembler.

$ jtool -arch x86_64 -q -d __DATA.__const  /System/Library/PrivateFrameworks/SkyLight.framework/SkyLight | grep "MIG sub"
Dumping from address 0x2b3e10 (Segment: __DATA.__const) to end of section
Address : 0x2b3e10 = Offset 0x2b4e10
0x2b4658: 68 74 00 00 a9 74 00 00 MIG subsystem 29800 (65 messages)
0x2b5110: 48 71 00 00 57 71 00 00 MIG subsystem 29000 (15 messages)
0x2b53f8: 10 72 00 00 13 74 00 00 MIG subsystem 29200 (515 messages)

Visiting each of these offsets in a disassembler, we’ll find cross-referencing code which will reveal the MIG dispatch handlers we want to hook. They may be different in new newer versions of Mojave – the above are from a 10.14 VM.

The first subsystem on our list is __CGXCGXWindowServer_subsystem, beginning 8 bytes before the jtool-provided offset in the SkyLight library.


All we need to do is ask our disassembler to show us references to the subsystem, and it is simple enough to identify the associated dispatch routine. Note that I am doing this in Hopper, but free alternatives such as radare2 or GHIDRA should work just fine.

dispatch routine hook target

After the pointer to the mach message we want to fuzz has been moved into rdi(0xcde63), the mach message handler pointed to by rax will be called. This call instruction is where we want to intercept. Note this first case is special because of a limitation in Frida that prevents us from hooking directly at the call instruction. This is because Frida needs 5 bytes of space after the intercept target to do its relocations, but as the basic block ends here there is no room. Fortunately for us we can just hook one instruction earlier, as at this point rdi already contains the pointer to the message we want to intercept. With the other two subsystems, be sure to hook at the call instruction.

To perform the intercept, we need to note the offset of instruction and the target call register, which in this case would be 0xcde66 and rax. Repeat this process with the two other MIG subsystems identified earlier. Now we can start putting together the JavaScript that will injected into the WindowServer process by Frida.

// code as provided by Ret2 Systems at
// intercept target offsets modified for Mojave

function InstallProbe(probe_address, target_register) {
    var probe = Interceptor.attach(probe_address, function(args) {
        var input_msg  = args[0]; // rdi (the incoming mach_msg)
        var output_msg = args[1]; // rsi (the response mach_msg)
        // extract the call target & its symbol name (_X...)
        var call_target = this.context[target_register];
        var call_target_name = DebugSymbol.fromAddress(call_target);
        // ready to read / modify / replay 
        console.log('[+] Message received for ' + call_target_name);
    return probe;

var targets = [
    ['0xcde66', 'rax'], // WindowServer_subsystem
    ['0x27d4a', 'rcx'], // Rendezvous_subsystem
    ['0xd0886', 'rax']  // Services_subsystem

// locate the runtime address of the SkyLight framework
var skylight = Module.findBaseAddress('SkyLight');
console.log('[*]  SkyLight @ ' + skylight);
// hook the target instructions
for (var i in targets) {
    var hook_address = ptr(skylight).add(targets[i][0]); // base + offset
    InstallProbe(hook_address, targets[i][1])
    console.log('[+] Hooked dispatch @ ' + hook_address);

Save the file, and run it with Frida. Since WindowServer essentially runs as root you’ll need to use sudo. Also note that by default even as root you will not be able to grant Frida permission to attach to the WindowServer process without first disabling System Integrity Protection.

$ sudo frida -l <fuzzer_file.js> WindowServer
[*]  SkyLight @ 0x7fff55353000
[+] Hooked dispatch @ 0x7fff55420e66
[+] Hooked dispatch @ 0x7fff5537ad4a
[+] Hooked dispatch @ 0x7fff55423886
[Local::WindowServer]-> [+] Message received for 0x7fff55391e61 SkyLight!_XRedrawLayerContext
[+] Message received for 0x7fff55391ed0 SkyLight!_XContextDidCommit
[+] Message received for 0x7fff55391e61 SkyLight!_XRedrawLayerContext
[+] Message received for 0x7fff55391ed0 SkyLight!_XContextDidCommit
[+] Message received for 0x7fff55391e61 SkyLight!_XRedrawLayerContext
[+] Message received for 0x7fff55391ed0 SkyLight!_XContextDidCommit
[+] Message received for 0x7fff55391e61 SkyLight!_XRedrawLayerContext
[+] Message received for 0x7fff55391ed0 SkyLight!_XContextDidCommit 

We are now able to intercept the mach messages as they are being passed to their respective dispatch routines.

reading the mach_msg_header_t struct

rdi(arg[0] in our interceptor function) points to the message’s mach_msg_header_t struct, which has various members that we need to read in order to be able to fuzz the message’s inline data and save information about it for our replay log. The following image describes the structure. Most of the members are simply types that boil down to 32-bit unsigned integers. This image from Chapter 9 of Amit Singh’s Mac OS X Internals gives an overview of the struct.

login fuzz

To read the message, we just use Frida’s readU32() method at the correct offsets within the struct, and readS32() for the msgh_id member.

// msgh_bits is unsigned int, offset: dec 0
var msgh_bits = args[0].readU32().toString(16);

// msgh_size is unsigned int, offset: dec 4
var msgh_size = args[0].add(4).readU32();

// msgh_remote_port is unsigned int, offset: dec 8
var msgh_remote_port = args[0].add(8).readU32();

// msgh_local_port is unsigned int, offset: dec 12
var msgh_local_port = args[0].add(12).readU32();

// msgh_voucher_port is unsigned int, offset: dec 16
var msgh_voucher_port = args[0].add(16).readU32();

// msgh_id is signed int, offset: dec 20
var msgh_id = args[0].add(20).readS32().toString(16);

// msgh_buffer is data of size msgh_size - 24, offset: dec 24
var msgh_buff = args[0].add(buff_pos).readByteArray(msgh_size - buff_pos);

fuzzing the inline message data

Now that we’ve read the message, we can fuzz the inline data contained in msgh_buff and write the fuzzed buffer into memory. You could skip reading the message data if you wish, but in order to make a replay log this information is necessary later on.

var flip_offset = Math.floor(Math.random() * msgh_buff.byteLength)); 
var flip_mask = rand(256);
var v = new DataView(msgh_buff, flip_offset, 1);
v.setInt8(0, (v.getInt8() ^ flip_mask));

// write the fuzzed buff

As you can see, we’re just doing a single bitflip somewhere in the message as demonstrated by Ret2, but you can of course change this to fuzz however you’d like.

At this point we can start a basic fuzzing session by running sudo frida -l <fuzzer_file.js> WindowServer as we did before. To keep this post brief I won’t describe how the replays work(big thanks to Markus at Ret2 for pointing me in the right direction on how to do it), but the full fuzz.js file with replay ability can be found on my github repo for this project. It includes the replay mode as well as a separate JavaScript file called driver.js that manages the fuzzing process, including the ability to reattach to WindowServer after a crash and log any exceptions found during fuzzing.


Chapter 9 on OSX IPC From Amit Singh’s Mac OS X Internals