Posted by Ian Beer, Project Zero
TL;DR
This was an exploit for a known bug class which I had been auditing for since late 2016. The same anti-pattern which lead to this vulnerability, we’ll see again in Exploit Chain #3, which follows this post.
This exploit chain targets iOS 10.3 through 10.3.3. Interestingly, I also independently discovered and reported this vulnerability to Apple, and it was fixed in iOS 11.2.
This also demonstrates that Project Zero’s work does collide with bugs being exploited in the wild.
In-the-wild iOS Exploit Chain 2 - IOSurface
targets: 5s through 7, 10.3 through 10.3.3 (vulnerability patched in 11.2)
iPhone6,1 (5s, N51AP)
iPhone6,2 (5s, N53AP)
iPhone7,1 (6 plus, N56AP)
iPhone7,2 (6, N61AP)
iPhone8,1 (6s, N71AP)
iPhone8,2 (6s plus, N66AP)
iPhone8,4 (SE, N69AP)
iPhone9,1 (7, D10AP)
iPhone9,2 (7 plus, D11AP)
iPhone9,3 (7, D101AP)
iPhone9,4 (7 plus, D111AP)
versions: (dates are release dates)
14E277 (10.3 - 27 Mar 2017)
14E304 (10.3.1 - 3 Apr 2017)
14F89 (10.3.2 - 15 May 2017)
14G60 (10.3.3 - 19 Jul 2017) <last version of iOS 10>
first unsupported version: 11.0 19 sep 2017
This bug wasn't patched until iOS 11.2, but they only supported iOS 10.3-10.3.3 (the last version of iOS 10.) For iOS 11 they moved to a new chain.
The kernel vulnerability
The kernel bug used here is CVE-2017-13861; a bug collision with Project Zero issue 1417, aka async_wake. I independently discovered this vulnerability and reported it to Apple on October 30th 2017. The attackers appears to have ceased using this bug prior to me finding it; the first unsupported version is iOS 11, released 19 September 2017. The bug wasn't fixed until iOS 11.2 however (released December 2nd 2017.)
The release of iOS 11 would have broken one of the exploitation techniques used by this exploit; specifically in iOS 11 the mach_zone_force_gc() kernel MIG method was removed. It's unclear why they moved to a completely new chain for iOS 11 (with a new trick for forcing GC after the removal of the method) rather than updating this chain.
The vulnerability
We saw in the first chain that IOKit external methods can be called via the IOConnectCallMethod function. There's another function you can call instead: IOConnectCallAsyncMethod, which takes an extra mach port and reference argument:
kern_return_t
IOConnectCallMethod(mach_port_t connection,
uint32_t selector,
const uint64_t* input,
uint32_t inputCnt,
const void* inputStruct,
size_t inputStructCnt,
uint64_t* output,
uint32_t* outputCnt,
void* outputStruct,
size_t* outputStructCnt);
vs
kern_return_t
IOConnectCallAsyncMethod(mach_port_t connection,
uint32_t selector,
mach_port_t wake_port,
uint64_t* reference,
uint32_t referenceCnt,
const uint64_t* input,
uint32_t inputCnt,
const void* inputStruct,
size_t inputStructCnt,
uint64_t* output,
uint32_t* outputCnt,
void* outputStruct,
size_t* outputStructCnt);
The intention is to allow drivers to send a notification message to the supplied mach port when an operation is completed (hence the "Async"(hronous) in the name.)
Since IOConnectCallAsyncMethod is a MIG method the lifetime of the wake_port argument will be subject to MIG's lifetime rules for mach ports.
MIG takes a reference on wake_port and calls the implementation of the MIG method (which will then call in to the IOKit driver's matching external method implementation.) The return value from the external method will be propagated up to the MIG level where the following rule will be applied:
If the return code is non-zero, indicating an error, then MIG will drop the reference it took on the wake_port. If the return code is zero, indicating success, then MIG will not drop the reference it took on wake_port, meaning the reference was transferred to the external method.
The bug was that IOSurfaceRootUserClient external method 17 (s_set_surface_notify) would drop a reference on the wake_port then also return an error code if the client had previously registered a port with the same reference value. MIG would see that error code and drop a second reference on the wake_port when only one reference was taken. This lead to the reference count being out-of-sync with the number of pointers to the port, leading to a use-after-free.
Again, this is directly reachable from inside the MobileSafari renderer sandbox due to this line in the sandbox profile:
(allow iokit-open
(iokit-user-client-class "IOSurfaceRootUserClient")
Setup
This exploit also relies on the system loader to resolve symbols. It uses the same code as Exploit Chain #1 to terminate all other threads in the current task. Before continuing on however, this exploit first tries to detect whether this device has already been exploited. It reads the kern.bootargs sysctl variable, and if the bootargs contains the string "iop1" then the thread goes into an infinite loop. At the end of the exploit we'll see them using the kernel memory read/write primitive they build to add the "iop1" string to the bootargs.
They use the same serialized NSDictionary technique to check whether this device and kernel version combo is supported and get the necessary offsets.
Exploitation
They call setrlimit with the RLIMIT_NOFILE resource parameter to increase the open file limit to 0x2000. They then create 0x800 pipes, saving the read and write end file descriptors. Note that by default iOS has a low default limit for the number of open file descriptors, hence the call to setrlimit.
They create an IOSurfaceRootUserClient connection; this time just used to trigger the bug rather than for storing property objects.
They call mach_zone_force_gc(), indicating that their initial resource setup is complete and they're going to start the heap groom.
Kernel Zone allocator garbage collection
This exploit introduces a new technique involving the mach_zone_force_gc host port method. In the first chain we saw the use of the kernel kalloc function for allocating kernel heap memory. The word heap is used here with its generic meaning of as "area used for scratch memory"; it has nothing to do with the classical heap data structure. The memory returned by kalloc is actually from a zone allocator called zalloc.
The kernel reserves a fixed-size region of its virtual address space for the kernel zone allocator and defines a number of named zones. The virtual memory region is then split up into chunks as zones grow based on dynamic memory allocation patterns. All zones return allocations of fixed sizes.
The kalloc function is a wrapper around a number of general-purpose fixed-sized zones such as kalloc.512, kalloc.6144 and so on. The kalloc wrapper function chooses the smallest kalloc.XXX zone size which will fit the requested allocation, then asks the zone allocator to return a new allocation from that zone. In addition to kalloc zones, many kernel subsystems also define their own special purpose zones. The kernel structures representing mach ports for example are always allocated from their own zone called ipc.ports. This is not intended to be a security mitigation (ala PartitionAlloc or GigaCage) but it does mean that an attacker has to take a few extra steps to build generic use-after-free exploits.
Over time zalloc zones can become fragmented. When there's memory pressure the zone allocator can perform a garbage collection. This has nothing to do with garbage collection in managed languages like java; the meaning here is much simpler: a zone GC operation involves finding zone chunks which consist of completely free allocations. Such chunks are removed from the particular zone (eg kalloc.4096) and made available to all zones again.
Prior to iOS 11 it was possible to force such a zone garbage collection to occur by calling the mach_zone_force_gc() host port MIG method. Forcing a zone GC is a very useful primitive as it enables the exploitation of a bug involving objects from one zone to using objects from another. This technique will be used in all subsequent kernel exploits we'll look at.
Let's return to the exploit. They allocate two sets of ports:
Set 1: 1200 ports
Set 2: 1024 ports
As we saw in the first chain, they're going to make use of mach message out-of-line memory descriptors for heap grooming. They make minor changes to the function itself but the principle remains the same, to make controlled-size kalloc allocations, the lifetimes of which are tied to particular mach ports. They call send_kalloc_reserver:
send_kalloc_reserver(v124, 4096, 0, 2560, 1);
This sends a mach message to port v124 with 2560 out-of-line descriptors, each of which causes a kalloc.4096 zone allocation. The contents of the memory aren't important here, initially they're just trying to fill in any holes in the kalloc.4096 zone.
Port groom
We've seen that the vulnerability involves mach ports, so we expect to see some heap grooming involving mach ports, which is what happens next. They allocate four more large groups of ports which I've named ports_3, ports_4, ports_5 and ports_6:
They allocate 10240 ports for the ports_3 group in a tight loop, then allocate a single mach port which we'll call target_port_1. They then allocate another 5120 ports for ports_4 in a second loop.
They're trying to force a heap layout like the following, where target_port_1 lies in an ipc_ports zone chunk where all the other ports in the chunk are from either ports_3 or ports_4. Note that due to the zone freelist mitigation introduced in iOS 9.2 there may be ports from both ports_3 and ports_4 before and after target_port_1:
They perform this same groom again, now with ports_5, then target_port_2, then ports_6: They send a send right to target_port_1 in an out-of-line ports descriptor in a mach message. Out-of-line ports, like out-of-line memory regions, will crop up again and again so it's worth looking at them in detail.
No comments:
Post a Comment