Memory corruption vulnerabilities are absolutely everywhere in modern exploit development. Something like 80% of bugs are memory corruption. This post goes into making the XNU (iOS & Mac backbone) OS memory allocator much harder to exploit via these types of vulnerabilities.
XNU has several memory allocator APIs but there are two main subsystems: the zone allocator for small chunks and the VM allocator which has page granularity and permission tracking; this post is focused on the zone allocator hardening. This allocator is a generic slab allocator, where a collection of pages are divided into equal chunks of data. There can be a special zone for a given use case, like ipc ports, which is a collection of chunks that can even be subdivided. When under memory pressure, chunks from a given zone may be reclaimed.
First, let's define memory safety in a few classes. Temporal safety is only using objects during their allocation timeline (UAF, double free). Spatial safety is only accessing memory that belongs to the particular chunk (buffer overflow, OOB read). Type safety is only using the object as the intended type (type confusion). Definite initialization is asserting that ALL allocations will be properly initialized (info disclosures). Finally, thread safety is ensuring concurrent access is done safely (race conditions).
In most exploits, a tiny (constrained) vulnerability is used to make a stronger primitive to eventually hijack the control flow. The first goal to prevent exploitation is type isolation, which things like GigaCage in WebKit pioneered. Preventing the access to specific data structures makes exploitation much more difficult. The second goal is to prevent the trivial overwriting of pointers with data. By isolating each of these as much as possible from each other, a buffer overflow with a user controlled string can no longer cause too much havoc. The rest of the article is explaining how these two systems are implemented.
An exploit UAF on iOS had the following flow:
- Allocate a bunch of objects then trigger a UAF on one of these to create a dangling reference.
- Free all of the objects for the target in step 1. This makes everything in the zone completely free.
- Create memory pressure that the zone containing the memory we want gets reused.
- Allocate a large number of objects, hopefully allocating over the memory from before. This creates a type confusion that can be easily exploited.
Since the path above was so reliable, it is time to mess this up. The first item is to make virtual address space reuse across zones (step 3 from the memory pressure) not possible. This was done by allowing for the reuse of physical memory but NOT the virtual memory for single-type zones. Now, simple overwrites of objects like thread and proc can no longer be exploited in this way.
The next step is to help isolate data from pointers. The first remediation step is introducing an allocation type that only contains data called KHEAP_DATA_BUFFERS that will live in its own section of memory. Secondly, a sized based collection of zones with a particular namespace of allocations.
The final step to making exploitation harder is a non-deterministic allocator. Of course, every type of object cannot have its own zone because of memory constraints. However, the group of objects put into each zone can be randomly selected at boot time, making exploitation inconsistent. They choose 200 zones to divide into different groups depending on the size.
For dynamically sized allocations, they disallowed the usage of a non-data header followed by a data-only type; this was to prevent trivially moving to the right zone for exploitation. Additionally, a number of heaps were exclusively created for these variable sized allocations. Finally, arrays of pointers had their own heap section as well.
So, we've done a ton of work to harden the allocator. How does this stack up to other things? For type isolation, IsoHeap is perfect (no reuse of zones) but kalloc_type has a large amount of buckets that are randomized for each size. Additionally, the heap metadata for kalloc_type is completely in a different section of memory, unlike others that store freelist pointers inline.
Overall, this is a great step forward for protecting the XNU kernel. With these mitigations making exploitation of heap issues harder and the presence of pointer authentication, XNU attacks will require extremely strong primitives from the beginning or a very sophisticated attacker.