What if we had the SockPuppet vulnerability in iOS 16?

Posted by Apple Security Engineering and Architecture (SEAR)

Our initial post about memory safety enhancements in the XNU kernel focused on kalloc_type — our new allocation API which provides randomized, bucketed type isolation to mitigate the exploitability of most use-after-free (UAF) vulnerabilities. In this post, we examine the practical effectiveness of kalloc_type against an illustrative vulnerability — a powerful, older and already-patched UAF bug known as SockPuppet.

The SockPuppet vulnerability was reported in 2019 by researcher Ned Williamson of Google Project Zero. It was present in several versions of iOS 12 and was addressed in iOS 12.3 and iOS 12.4.1. This vulnerability is a particularly useful case study for our kernel allocator hardening work because so many aspects of the bug worked in the attacker’s favor: the UAF trigger and reuse lifetimes were fully attacker-controlled, the UAF’d structure offered many paths to exploitation, and a built-in memory disclosure made heap grooming extremely stable.

After analyzing the available reallocation types under kalloc_type, we believe that it would be significantly harder to build even a semi-reliable exploit for SockPuppet were it still present in iOS 16. The conventional approach of using a single reallocation type would have just an 8% success rate under kalloc_type, and our analysis suggests that attackers will run into a fundamental reliability cap for many use-after-free vulnerabilities. For example, we estimate that SockPuppet has an inherent exploit success rate limit of around 92%, meaning that even the best possible exploit that selects which exploit strategy to use based on the viable reallocation types would fail about 8% of the time. Since the original SockPuppet exploit used just a single exploit strategy and was almost 100% reliable, we believe that kalloc_type would make SockPuppet considerably less attractive to exploit.

The SockPuppet vulnerability

The SockPuppet vulnerability was a use-after-free in the XNU kernel's in6_pcbdetach() function that was reachable through a series of socket-related syscalls. This function failed to clear the inp->in6p_outputopts field after freeing it and left the socket with a dangling pointer to a ip6_pktopts struct. As a result, various getsockopt() and setsockopt() operations could perform limited reads and writes to the freed ip6_pktopts object, as well as read and write through pointers dereferenced from the freed object.

if (!(so->so_flags & SOF_PCBCLEARING)) {
...
if (inp->in6p_options != NULL) {
m_freem(inp->in6p_options);
inp->in6p_options = NULL;
}
ip6_freepcbopts(inp->in6p_outputopts);
+ inp->in6p_outputopts = NULL;
ROUTE_RELEASE(&inp->in6p_route);
/* free IPv4 related resources in case of mapped addr */
if (inp->inp_options != NULL) {
(void) m_free(inp->inp_options);
inp->inp_options = NULL;
}
...
}

Importantly, the ip6_pktopts struct could be freed only once using the vulnerability, which meant that all memory corruption had to occur by manipulating the freed memory through the ip6_pktopts view.

The original exploit strategy

The SockPuppet exploit used the following basic primitives granted by the vulnerability and the socket-related system calls:

  • getsockopt(IPV6_USE_MIN_MTU): This syscall would read the 4-byte ip6po_minmtu field at offset 0xb4 in the ip6_pktopts object.
  • getsockopt(IPV6_PREFER_TEMPADDR): This syscall would read the 4-byte ip6po_prefer_tempaddr field at offset 0xb8 in the ip6_pktopts object.
  • getsockopt(IPV6_PKTINFO): This syscall would read 20 bytes from the address pointed to by the ip6po_pktinfo field at offset 0x10 in the ip6_pktopts object.
  • setsockopt(IPV6_PKTINFO): This syscall would free the ip6po_pktinfo pointer at offset 0x10 in the ip6_pktopts object.
A diagram showing the relationship between the structures involved in the SockPuppet vulnerability. The inpcb struct contains a pointer to the ip6_pktopts struct which will be left dangling. The ip6_pktopts struct contains both pointer and data fields that can be manipulated after the struct has been freed.

Out of these basic primitives, the SockPuppet exploit built the following capabilities:

  1. Disclose the address of an arbitrary Mach port: The exploit created a socket with a dangling ip6_pktopts field, then sprayed a large number of Mach messages containing out-of-line Mach ports arrays of the same size as the ip6_pktopts struct. This spray reallocated the dangling ip6_pktopts object with one of those out-of-line ports arrays. By filling the Mach message’s port array in userspace with many references to the target Mach port, the ip6po_minmtu and ip6po_prefer_tempaddr fields would overlap the upper and lower halves of two adjacent pointers, respectively. Calling getsockopt(IPV6_USE_MIN_MTU) and getsockopt(IPV6_PREFER_TEMPADDR) to read those fields then allowed the exploit to reconstruct the full Mach port address.
  2. Read 20 bytes at an arbitrary kernel address: The exploit created a socket with a dangling ip6_pktopts field, then sprayed a large number of OSData buffers of the same size as the ip6_pktopts struct. This spray reallocated the dangling ip6_pktopts object with one of those OSData buffers. By setting offset 0x10 in the data buffer to the target address and offset 0xb4 to a magic value, the exploit was able to fully control the values of the ip6_pktopt’s ip6po_pktinfo and ip6po_minmtu fields. The exploit then called getsockopt(IPV6_USE_MIN_MTU) to check that ip6po_minmtu indeed contained the magic value, which indicated that the dangling ip6_pktopts structure had been successfully reallocated and thus ip6po_pktinfo contained the target address. Finally, the exploit called getsockopt(IPV6_PKTINFO) to copy 20 bytes from that address out to userspace.
  3. Free an arbitrary kernel address: This primitive worked just like the kernel read primitive above, except that in the last step, rather than calling getsockopt(IPV6_PKTINFO) to read from the target address, the exploit called setsockopt(IPV6_PKTINFO) to pass the target address to XNU’s kfree() function.

The first primitive relied on reallocating a dangling ip6_pktopts struct with an out-of-line Mach ports array, which is an array of pointers. The second and third primitives relied on reallocating an ip6_pktopts struct with an OSData buffer, a data allocation containing no pointers.

And now we begin to see how kalloc_type can help. As of iOS 16, out-of-line Mach ports arrays belong to a kalloc_type_var zone, and OSData buffers are allocated within the data submap. None of the primitives above would work under kalloc_type because the replacement allocations are served from separate parts of the virtual address space than the ip6_pktopts allocation. And even if we had some other mechanism to construct the arbitrary free primitive, kfree_type() now expands to a zfree(kt_view, ptr) call that frees the “arbitrary” pointer to a specific zone and panics if the passed-in pointer isn’t from that zone.

The kalloc_type landscape

Breaking an exploit primitive isn’t useful if there’s still an easy way to rebuild the primitive using a slightly modified technique, for example by targeting a different field in the UAF’d struct or choosing a different replacement allocation. A truly effective mitigation would limit the exploitability of the SockPuppet vulnerability for all exploit strategies, not just the one in the original report. Let’s evaluate what kalloc_type achieves in this regard.

We can pretend that the SockPuppet vulnerability is present in the iOS 16 beta 1 (build 20A5283p)—after kalloc_type was introduced—and redo the analysis of basic primitives we gain by controlling the various fields of a dangling ip6_pktopts struct through reallocation. We're simplifying our analysis for brevity, and we’ll err on the side of assuming something is exploitable unless we can show that it isn’t.

The ip6_pktopts struct is exactly 192 (0xc0) bytes in size and will be assigned to a regular kalloc_type zone like kalloc.typeN.192. On an iPhone 13 Pro running iOS 16 beta 1, there are 194 regular (not variable-length) types in that size class, with 119 unique signatures and 90 signature groups among them. The 90 signature groups are distributed evenly among 11 zones, named kalloc.type0.192 through kalloc.type10.192. And since the allocator metadata for kalloc_type lives entirely outside the allocated region, these 194 types are the only things that could ever colocate with the ip6_pktopts struct.

(lldb) showkalloctypes -S 192
Zone: kalloc.type0.192 (0xfffffff02364ae98)
kalloc_type_view typename signature
0xfffffff0233e96b8 IOSurfaceMemoryPool 1211221111111111111121
0xfffffff02340ddc8 IOUSBHostPipe 12111111221211112222212
...
Zone: kalloc.type8.192 (0xfffffff02364b3d8)
kalloc_type_view typename signature
0xfffffff023386da8 IOGPUSysMemory 121222221211122112112
0xfffffff023251c10 IOPearlCameraFrame 121122222222212222221
0xfffffff02339db68 IOHIDReportElementQueue 121112222222222222211
0xfffffff0231872b8 AppleAudioFirmwareDownloader 121111121222121212112111
0xfffffff02326e098 AppleSEPARTService 12111112122212121211211
0xfffffff0232c1000 AppleCBTL1610Companion 121111121222121212112
0xfffffff023103310 AppleAuthCPI2C 121111121222121212112
0xfffffff02314b2d0 BTDebugReporter 1211222222222111111221
0xfffffff0230d5288 AppleAOPAudioDeviceProvider 12111112122212121112122
0xfffffff02308b320 struct ip6_pktopts 121111122221111112222122
0xfffffff0231df978 HIDInterface 121111122111122111122111
Zone: kalloc.type9.192 (0xfffffff02364b480)
...

In this particular boot, the ip6_pktopts struct landed in bucket 8 along with 10 other types.

Importantly for the rest of our analysis, ip6_pktopts does not share a signature group with any other type, so there is no other struct that is guaranteed to always be allocated in the same bucket. If there were another type in the same signature group as ip6_pktopts, then that type would be the primary reallocation candidate, since it could always be used to reallocate ip6_pktopts. However, sharing a signature group would also mean that the two structs have pointers and data in the same slots, which should eliminate the easy route of finding a pointer/data overlap between the two structs, assuming that the signature is accurate.

Struct fields and useful overlaps

There are many interesting fields in the ip6_pktopts struct for building read or write gadgets:

  1. ip6po_hlim (0x8): This 4-byte field can be written with any value in the range 0-255.
  2. ip6po_pktinfo (0x10): 20 bytes at the address pointed to by this field can be read. The address can either be written with 16 zero bytes followed by a 4-byte integer roughly in the range 1-25 (the allowed range may vary), or the address can be freed to the data heap with kfree_data(pktinfo, 32), which frees to the data.kalloc.32 zone.
  3. ip6po_nhi_nexthop (0x18): A data-dependent number of bytes at the address pointed to by this field can be read. With root privileges, the address can be freed with a call to kfree_data_addr(nexthop) and optionally the field can be overwritten with a pointer to a new allocation that contains attacker-controlled data.
  4. ip6po_hbh (0x58): This field behaves similarly to ip6po_nhi_nexthop.
  5. ip6po_dest1 (0x60): This field behaves similarly to ip6po_nhi_nexthop.
  6. ip6po_rhi_rthdr (0x68): This field behaves similarly to ip6po_nhi_nexthop.
  7. ip6po_dest2 (0xa8): This field behaves similarly to ip6po_nhi_nexthop.
  8. ip6po_tclass (0xb0): This 4-byte field can be read if it is non-negative and can be written with any value in the range 0-255.
  9. ip6po_minmtu (0xb4): This 4-byte field can be read and can be written with the values -1, 0, and 1.
  10. ip6po_prefer_tempaddr (0xb8): This field behaves similarly to ip6po_minmtu.
  11. ip6po_flags (0xbc): This 4-byte field can be partially read and written bitwise.

That’s quite a large number of candidates for building an exploit strategy. And each of these accesses is reached by an independent syscall, so they can be mixed and matched arbitrarily.

Here we can see how all of the types and signature groups line up with each of those fields in ip6_pktopts. We list both because the attacker cares about which types can form an exploitable overlap, but the allocator assigns types into buckets based on signature groups, which are much coarser: any types that belong to the same signature group will always belong to the same bucket.

FieldOffsetPrimitive"1" types"2" types"1" groups"2" groups
ip6po_hlim0x8W17775
ip6po_pktinfo0x10Ptr R/W/free173217317
ip6po_nhi_nexthop0x18Ptr R/free+replace168246821
ip6po_hbh0x58Ptr R/free+replace401542763
ip6po_dest10x60Ptr R/free+replace156385634
ip6po_rhi_rthdr0x68Ptr R/free+replace381562763
ip6po_dest20xa8Ptr R/free+replace174683341
ip6po_tclass0xb0R/W4429
ip6po_minmtu0xb4R/W4429
ip6po_prefer_tempaddr0xb8R/W2619
ip6po_flags0xbcR/W2619

We can see, for instance, that there are 173 different C/C++ types, belonging to 73 different signature groups, with a pointer (“1”) in the field that overlaps ip6po_pktinfo. Why is this useful? It illustrates which fields are more likely to have useful collisions. For example, if every other type in ip6_pktopts's bucket has a pointer in the granule overlapping ip6po_pktinfo, it will always be safe to dereference that field. Using the information in the table, we can compute that there's a 23% chance that the bucket arrangement would guarantee that dereferencing ip6po_pktinfo is always safe.

Exploitation properties

A few things stand out when starting to analyze these fields for exploitation.

First, there are so many options for both direct and indirect reading that we will generously assume that the attacker can always build an arbitrary kernel read primitive, and in particular that the attacker can tell which types share a bucket with ip6_pktopts.

Next, the direct-write primitives (the ones that write to the ip6_pktopts allocation itself, rather than through a pointer within that allocation) are all unsuitable for manipulating overlapping pointers: the ip6po_flags field can only shift pointers by at least 4GB, which seems too coarse to be promising under the kernel’s memory layout, while the other direct-write fields are all value-constrained so that 3 of the 8 bytes of the resulting pointer would not be controlled.

That said, the ip6po_hlim field is interesting because it overlaps the retainCount field of OSObject, which is the base type for almost all C++ objects in the kernel. Reallocating the ip6_pktopts with any OSObject-based type and writing to ip6po_hlim could introduce a new UAF in that OSObject, which might allow transferring the UAF to another bucket.

But the most promising avenue for building read/write are the 6 indirect primitives. The ip6po_pktinfo primitive can either zero out roughly 20 bytes of memory or free a data.kalloc.32 pointer, while the other primitives can free and replace a (possibly NULL) data.kalloc pointer with a new allocation containing controlled data.

Replacement candidates in XNU

As a rough approximation for the set of all 194 types, the table below displays all core XNU types that could ever colocate with ip6_pktopts and also the fields of each that could potentially be useful for an exploit. The “Allocatable” column describes whether userspace can allocate instances of this type, and the “Privileges” column describes any special privileges needed to do so. The other columns represent the fields of ip6_pktopts, and the color of each cell represents how the type confusion would manipulate the overlapped field of the replacement type. All empty cells were deemed uninteresting.

Core XNU types and their potential to be exploited.
TypeAllocatablePrivileges0x8 (d)0x10 (p)0x18 (p)0x58 (p)0x60 (p)0x68 (p)0xa8 (p)0xb0 (d)0xb8 (d)
_vector_uplYesNoneDoesn't create exploitable condition.Is of the wrong or will be re-initialized to the wrong type before use.Is not controllable enough to be a useful pointer.Is of the wrong or will be re-initialized to the wrong type before use.Is of the wrong or will be re-initialized to the wrong type before use.Could be targeted with primitive.Could contain a controlled pointer.
necp_client_flow_registrationYesNoneIs of the wrong or will be re-initialized to the wrong type before use.Could contain a controlled pointer.Is not controllable enough to be a useful pointer.Is not controllable enough to be a useful pointer.Could be targeted with primitive.Could be targeted with primitive.
soflow_hash_entryYesNoneIs of the wrong or will be re-initialized to the wrong type before use.Is of the wrong or will be re-initialized to the wrong type before use.Could contain a controlled pointer.Could contain a controlled pointer.Could contain a controlled pointer.Is not controllable enough to be a useful pointer.Is not controllable enough to be a useful pointer.
nxctl_traffic_rule_inetYesRoot or entitledDoesn't create exploitable condition.Is not controllable enough to be a useful pointer.Is not controllable enough to be a useful pointer.Could contain a controlled pointer.Could contain a controlled pointer.Could contain a controlled pointer.Doesn't create exploitable condition.
dn_flow_queueYesRootCould contain a controlled pointer.Is not controllable enough to be a useful pointer.Could contain a controlled pointer.Could contain a controlled pointer.
dn_flow_setYesRootDoesn't create exploitable condition.Could be targeted with primitive.Could contain a controlled pointer.Could be targeted with primitive.Could contain a controlled pointer.Doesn't create exploitable condition.
dyld_pagerHardNoneIs of the wrong or will be re-initialized to the wrong type before use.Could contain a controlled pointer.Could contain a controlled pointer.Could contain a controlled pointer.Could contain a controlled pointer.
vm_shared_regionHardNoneIs of the wrong or will be re-initialized to the wrong type before use.Could be targeted with primitive.Is of the wrong or will be re-initialized to the wrong type before use.Is not controllable enough to be a useful pointer.Is not controllable enough to be a useful pointer.
IOStateReporterMaybeSandbox?Could be targeted with primitive.Is not controllable enough to be a useful pointer.Could be targeted with primitive.Is not controllable enough to be a useful pointer.
IOHistogramReporterMaybeSandbox?Could be targeted with primitive.Is not controllable enough to be a useful pointer.Could be targeted with primitive.Is not controllable enough to be a useful pointer.
IOPowerConnectionMaybeSandbox?Could be targeted with primitive.Is of the wrong or will be re-initialized to the wrong type before use.Is of the wrong or will be re-initialized to the wrong type before use.Is not controllable enough to be a useful pointer.Is of the wrong or will be re-initialized to the wrong type before use.Is not controllable enough to be a useful pointer.Doesn't create exploitable condition.Doesn't create exploitable condition.
lck_grp_tMaybeSandbox?Is not controllable enough to be a useful pointer.Is not controllable enough to be a useful pointer.
IOCPUNo
IOCPUInterruptControllerNo
IOPMWorkQueueNo
IOPolledFileIOVarsNo
protoswNo
KeyTargeting this field doesn't seem to create an exploitable condition.
This pointer field is of the wrong type or will be re-initialized to the wrong type before use.
This data field's value is not controllable enough to be a useful pointer.
This data field could potentially be made to contain a controlled pointer value; this might be able to create an arbitrary read primitive.
This pointer/data field could be manipulated with the corresponding primitive; this might be able to create an arbitrary write primitive.

As an example, consider targeting offset 0x10 in necp_client_flow_registration. The ip6_pktopts primitive we have to manipulate offset 0x10 allows us to either read 20 bytes at the address stored in that field, write 20 uncontrolled bytes to that address, or free that address to the data heap. Offset 0x10 of an necp_client_flow_registration object is a red-black tree entry, fd_link.rbe_parent, which is a pointer to another necp_client_flow_registration. We cannot use this type confusion directly as an arbitrary read because we do not control the value of that pointer: it will be set to a valid red-black tree entry when the object is created. We also don’t get any useful effects by using the write primitive, since the first 20 bytes only contain other red-black tree entries and our write primitive can’t produce a valid pointer value. Finally, the free primitive won’t work because we’d be freeing a kalloc_type-owned pointer to the data.kalloc.32 zone, which will panic. This is why the cell in column 0x10 is colored orange. By contrast, the cell in column 0x18 is green because there is a plausible exploitation path: if fd_link.rbe_left is already zero, then the free-and-reallocate-with-controlled-data primitive that we have for offset 0x18 could be used to insert a fake necp_client_flow_registration into the red-black tree.

The most interesting fields are the blue and green ones. Blue indicates that the value of a field could potentially be controlled to contain a pointer, while green indicates that the field could potentially be manipulated with the write primitive. Neither color means that the field can definitely be used to create a kernel read or write primitive: many fields will turn out to be dead ends due to constraints imposed by the surrounding code. However, the colors should provide an upper bound on the capabilities gained by targeting each field of each type.

Let’s look at some statistics from this table.

  • 13 of the 18 XNU types (72%) are potentially allocatable. 7 of those (39%) are likely to be allocatable in a manner conducive to heap spraying. 4 of those (22%) likely don’t need any special privileges to allocate them.
  • 6 of the 12 replacement candidates (50%) have some field that could potentially be used to build an arbitrary read gadget. 5 of those (42%) are likely sprayable, and 2 of those (17%) don’t need privileges.
  • 7 of the 12 replacement candidates (58%) have some field that could potentially be used to build a write gadget. 3 of those (25%) are likely sprayable, and 2 of those (17%) don’t need privileges to allocate. However, if we also consider that certain fields of ip6_pktopts require root privileges to modify, the number of sprayable replacement candidates that don’t need special privileges to potentially build a write primitive drops to 1 (8%).

The table and statistics above are likely upper bounds for building an actual exploit. For example, IOPowerConnection objects should be allocated only when a new device requiring power is attached to the system, so they should be very hard for a remote attacker to allocate. Also, it seems unlikely that modifying the retainCount at offset 0x8 of such an object would actually be useful, given how that type is used.

Another complication is that several types and fields can only be accessed under elevated privileges. For example, the dn_flow_set and dn_flow_queue types are both part of the dummynet feature (see dnctl(8)), which requires root privileges. And the entire vertical middle block of the table—corresponding to offsets 0x18 through 0xa8—requires root privileges to perform the write gadget via ip6_pktopts, so both the rows and columns of the table look much sparser to an unprivileged attacker.

What might reliable exploitation look like?

To make the analysis tractable, we’ll assume that the set of replacement candidates from core XNU is representative of the full size class for the whole kernelcache. This is an approximation, but we believe it to be conservative because most IOKit types should have more limited reachability than core XNU.

In the most generous interpretation, which serves as an upper bound, we expect 68 of the 194 types in the kernelcache to offer a potentially useful read primitive and 80 types to offer a potential path to turn the write gadget into further memory corruption. The signature groups for those types are randomly distributed among the 11 buckets of the size class. There’s no advantage to having multiple exploitable types within a signature group, so we focus on the number of exploitable signature groups instead. Based on the size of each signature group in this size class, the 68 types with useful pointer/data overlaps fall on average into 44 signature groups, and the 80 types that could build a write primitive fall into 49 groups.

Of course, we expect that the actual number of reachable types will be much smaller in practice. If we extrapolate based on the number of XNU types that are reasonably likely to be sprayable, we instead expect there to be 38 signature groups containing types that offer potential read primitives and 26 signature groups containing types that offer potential write primitives. And if we consider an attack from an unprivileged position, this further reduces to just 19 signature groups with potential read primitives and 10 signature groups with potential write primitives.

Now, how many candidate replacement types would we need to achieve a fixed probability that one of the candidates shares a bucket with ip6_pktopts? If we do the math, we’d need 15 candidates for a 75% chance of collision and 30 candidates for a 95% chance of collision. That is, if you wanted your exploit to succeed at least 95% of the time, you’d need to write code to allocate 30 different types from the available signature groups, corrupt 30 different fields using the ip6_pktopts primitives, and then from each of those slightly different corrupted states build a useful kernel write primitive — all while knowing that you'll learn which of those 30 candidates will actually work on the target device only after the exploit starts running.

The conservative upper-bound analysis suggests there could be up to about 49 signature groups to build a write primitive, so this is theoretically achievable. However, if we take the more realistic estimate of 26 signature groups for types that can actually be allocated in practice, we no longer have enough signature groups to guarantee a 95% success rate: 26 replacement types affords only a 92% chance of collision, meaning that the best possible exploit that uses all 26 possible replacement types would still fail on at least 8% of booted systems. And the situation is even worse from an unprivileged position. Without elevating privileges some other way first, trying to exploit this kernel bug gives an estimated 59% success probability, meaning that any exploit would fail at least 41% of the time.

There are three key takeaways from this analysis.

First, even though there are many potential replacement types, we estimate that in practice the maximum probability of success achievable by any exploit for this bug is bounded near 92%. Put another way, inherent constraints in types that can be allocated and types that can be used to replace the ip6_pktopts allocation mean that the very best exploit that leverages all available avenues will still likely fail on about 8% of booted systems.

Second, kalloc_type and sandboxing complement each other and make exploiting this bug from an unprivileged position substantially harder. We estimate the unprivileged exploit success probability might be bounded to around 59%. For attackers seeking to reliably exploit a kernel UAF, it may be more appealing to attempt a sandbox escape first to get access to a wider set of replacement types.

Third, the XNU types did not appear so similar that there was an obvious way to build a generalized exploit technique that was shared among multiple replacement types. An exploit writer would likely have to develop close to 15 separate exploit strategies to be able to use all 15 replacement types needed to achieve 75% reliability. Developing 15 exploit strategies with different replacement types for the same bug wouldn’t be as much work as writing 15 separate exploits, since some code could be reused between different strategies and the exploit flows would eventually converge on common primitives. Nevertheless, this represents a radical and potentially prohibitive increase in the amount of required exploit development effort compared to exploiting a pre-kalloc_type system, where exploits could sometimes reuse exploit strategies and exploit flows even across different vulnerabilities.

Conclusions

The SockPuppet vulnerability appears significantly harder to exploit under kalloc_type. With kalloc_type’s randomized bucketing, the only reliable exploit strategies we are aware of would require writing several different exploits and then dynamically selecting which one to deploy based on the boot-time bucket assignment of the affected types. Furthermore, we expect that in practice there aren’t enough reachable replacement types to guarantee high reliability, especially without first escaping the sandbox.

Looking beyond SockPuppet, this analysis confirms that signature collisions are an important factor in understanding the exploitability of a given UAF and that targeting pointer/pointer overlaps is likely to be practical. For UAFs in sparse size classes, we expect that the practically achievable exploit success rate is likely to be bounded by the number of reachable replacement types, and that sandboxing in particular is a strong limiting factor. Of course, signature collisions, the data submap, and within-type UAFs are all still attractive targets under kalloc_type. However, we expect that continued adoption and refinement of kalloc_type will make exploitation of most kernel use-after-free vulnerabilities unattractive.



We would like to thank Ned Williamson for reviewing and providing feedback on this post.