September 5, 2019
Last week, Google published a series of blog posts detailing five iOS exploit chains being used in the wild that were found by Google’s Threat Analysis Group (TAG) team back in February. The first part of my write-up was an overview of the different stages in the first exploit chain. In this part, we will look at the iOS kernel heap and how the exploit developer uses the heap overflow in this exploit.
Before talking about the specific heap overflow used in the exploit, let’s first do a quick recap on heaps in general, the iOS kernel heap in particular, and how heap overflows actually work.
Heaps, in the context of programs written in languages like C and C++, allow functions to allocate a temporary exclusive region of memory to store variables and structured data. Once allocated, these memory regions are permanently reserved until the memory is manually released by the programmer back to the heap manager, at which point the heap manager can “recycle” it for use by another bit of code.
In order for programs to operate correctly, it is critically important that programmers using the heap follow a few simple rules. For example, they must avoid writing to addresses outside of the allocated region, and they must be careful to never use or write to data in an allocation that has been released back to the heap.
Failure to follow these rules invalidates the heap’s exclusivity guarantee. This can lead to different variables ending up unintentionally aliased to the same underlying memory addresses. Since those variables might have very different security properties, like one containing data to be processed and another containing security critical properties like function pointers, this can very often be exploited by exploit developers to take control of the process — in this case, the process taken over is the iOS operating system kernel itself.
In this iOS exploit, the vulnerability around which the exploit is built is in a graphics driver, and occurs on an object allocated via the IOMalloc function. This allocator uses the kalloc allocator under the hood, and will therefore a heap overflow of this object will spill data onto adjacent kalloc-allocated objects.
The kalloc heap allocator works in a very different way to the glibc heap allocator, which I’ve discussed in two previous posts, but its broad purpose is still basically the same: it allows kernel driver developers to allocate and deallocate memory for variables during the normal course of managing the system.
One of the best primers on how the iOS kernel’s kalloc heap allocator works is by Stefan Esser, and can can be found here, but there are a few things we need to highlight. First of all, kalloc allocations are private to the operating system itself. The location and contents of these allocations are not visible to apps running on the device.
Second, objects allocated with kalloc traditionally didn’t use heap metadata between live objects to track things like allocation chunk lengths. When a kalloc heap allocation is released, the programmer must remind kfree of the original allocation length so that kfree can recycle it for other allocations. If this length was misremembered, this would lead to a vulnerability category called a zone-transfer, leading to a heap overflow, although iOS since iOS 9 now has mitigations in place to defend against this attack category. For this reason, iOS kernel developers often use kalloc indirectly through easier-to-use APIs such as _MALLOC, even though the allocation is still handled internally by kalloc.
The kalloc allocator internally uses a zone allocation strategy (implemented via zalloc), which has the effect that allocations cluster together based on their size. For example, an allocation of size 4096 bytes will be allocated in the kalloc.4096 zone alongside other 4096-byte allocations, whereas an allocation of size 2048 will be allocated far away in the kalloc.2048 zone. This means that when a heap overflow on a 2048 byte kalloc-allocated object occurs, the adjacent victim object that gets corrupted will be of a similar size in the kalloc.2048 zone.
In this particular exploit, the vulnerability occurs on an object whose length the exploit developer can influence. The exploit developer therefore can choose which kalloc zone the vulnerability will be triggered in. The exploit developer chooses to arrange for all of the interesting objects to be allocated in the kalloc.4096 zone. This zone is a bit quieter than the smaller zones, since programs tend to allocate and deallocate small allocations much more frequently than larger allocations. Choosing this zone therefore makes the exploit developer’s life a little bit easier.
The specific vulnerability in this exploit is nothing particularly special; the author of a graphics kernel driver made a bad assumption about a complex input data structure and performs a heap allocation whose size is based on this faulty assumption. By providing an input that violates this assumption, the kernel is tricked into writing more entries into that heap allocation than it had planned for, and the final few entries can be made to spill over onto an unsuspecting object adjoining it on the kernel’s heap.