... Loading ...

SSD Secure Disclosure

Disclosing vulnerabilities responsibly since 2007

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

Introduction:
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.

CVEs
CVE-2019-8797
CVE-2019-8795
CVE-2019-8794

Credit
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 (com.apple.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, com.apple.midiserver and com.apple.midiserver.io. The former is a typical MIG-based Mach server, which implements 47 methods (as of writing). com.apple.midiserver.io 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:

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:

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:

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

Exploitation

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:

And the heap is crafted here:

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:

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 com.apple.midiserver.io Mach server, with the ID of our target MIDIEntity object (mentity_id):

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:

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:

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:

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:

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:

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:

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):

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:

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

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:

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:

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

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:

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!

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:

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

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

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:

Panic log:

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

Exploitation

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.

Print Friendly, PDF & Email

Leave a Reply