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-byteip6po_minmtu
field at offset0xb4
in theip6_pktopts
object.getsockopt(IPV6_PREFER_TEMPADDR)
: This syscall would read the 4-byteip6po_prefer_tempaddr
field at offset0xb8
in theip6_pktopts
object.getsockopt(IPV6_PKTINFO)
: This syscall would read 20 bytes from the address pointed to by theip6po_pktinfo
field at offset0x10
in theip6_pktopts
object.setsockopt(IPV6_PKTINFO)
: This syscall would free theip6po_pktinfo
pointer at offset0x10
in theip6_pktopts
object.
Out of these basic primitives, the SockPuppet exploit built the following capabilities:
- 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 theip6_pktopts
struct. This spray reallocated the danglingip6_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, theip6po_minmtu
andip6po_prefer_tempaddr
fields would overlap the upper and lower halves of two adjacent pointers, respectively. Callinggetsockopt(IPV6_USE_MIN_MTU)
andgetsockopt(IPV6_PREFER_TEMPADDR)
to read those fields then allowed the exploit to reconstruct the full Mach port address. - Read 20 bytes at an arbitrary kernel address: The exploit created a socket with a dangling
ip6_pktopts
field, then sprayed a large number ofOSData
buffers of the same size as theip6_pktopts
struct. This spray reallocated the danglingip6_pktopts
object with one of thoseOSData
buffers. By setting offset0x10
in the data buffer to the target address and offset0xb4
to a magic value, the exploit was able to fully control the values of theip6_pktopt
’sip6po_pktinfo
andip6po_minmtu
fields. The exploit then calledgetsockopt(IPV6_USE_MIN_MTU)
to check thatip6po_minmtu
indeed contained the magic value, which indicated that the danglingip6_pktopts
structure had been successfully reallocated and thusip6po_pktinfo
contained the target address. Finally, the exploit calledgetsockopt(IPV6_PKTINFO)
to copy 20 bytes from that address out to userspace. - 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 calledsetsockopt(IPV6_PKTINFO)
to pass the target address to XNU’skfree()
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 192Zone: kalloc.type0.192 (0xfffffff02364ae98)kalloc_type_view typename signature0xfffffff0233e96b8 IOSurfaceMemoryPool 12112211111111111111210xfffffff02340ddc8 IOUSBHostPipe 12111111221211112222212...Zone: kalloc.type8.192 (0xfffffff02364b3d8)kalloc_type_view typename signature0xfffffff023386da8 IOGPUSysMemory 1212222212111221121120xfffffff023251c10 IOPearlCameraFrame 1211222222222122222210xfffffff02339db68 IOHIDReportElementQueue 1211122222222222222110xfffffff0231872b8 AppleAudioFirmwareDownloader 1211111212221212121121110xfffffff02326e098 AppleSEPARTService 121111121222121212112110xfffffff0232c1000 AppleCBTL1610Companion 1211111212221212121120xfffffff023103310 AppleAuthCPI2C 1211111212221212121120xfffffff02314b2d0 BTDebugReporter 12112222222221111112210xfffffff0230d5288 AppleAOPAudioDeviceProvider 121111121222121211121220xfffffff02308b320 struct ip6_pktopts 1211111222211111122221220xfffffff0231df978 HIDInterface 121111122111122111122111Zone: 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:
ip6po_hlim
(0x8
): This 4-byte field can be written with any value in the range 0-255.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 withkfree_data(pktinfo, 32)
, which frees to thedata.kalloc.32
zone.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 tokfree_data_addr(nexthop)
and optionally the field can be overwritten with a pointer to a new allocation that contains attacker-controlled data.ip6po_hbh
(0x58
): This field behaves similarly toip6po_nhi_nexthop
.ip6po_dest1
(0x60
): This field behaves similarly toip6po_nhi_nexthop
.ip6po_rhi_rthdr
(0x68
): This field behaves similarly toip6po_nhi_nexthop
.ip6po_dest2
(0xa8
): This field behaves similarly toip6po_nhi_nexthop
.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.ip6po_minmtu
(0xb4
): This 4-byte field can be read and can be written with the values -1, 0, and 1.ip6po_prefer_tempaddr
(0xb8
): This field behaves similarly toip6po_minmtu
.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.
Field | Offset | Primitive | "1" types | "2" types | "1" groups | "2" groups |
---|---|---|---|---|---|---|
ip6po_hlim | 0x8 | W | 177 | 75 | ||
ip6po_pktinfo | 0x10 | Ptr R/W/free | 173 | 21 | 73 | 17 |
ip6po_nhi_nexthop | 0x18 | Ptr R/free+replace | 168 | 24 | 68 | 21 |
ip6po_hbh | 0x58 | Ptr R/free+replace | 40 | 154 | 27 | 63 |
ip6po_dest1 | 0x60 | Ptr R/free+replace | 156 | 38 | 56 | 34 |
ip6po_rhi_rthdr | 0x68 | Ptr R/free+replace | 38 | 156 | 27 | 63 |
ip6po_dest2 | 0xa8 | Ptr R/free+replace | 174 | 68 | 33 | 41 |
ip6po_tclass | 0xb0 | R/W | 44 | 29 | ||
ip6po_minmtu | 0xb4 | R/W | 44 | 29 | ||
ip6po_prefer_tempaddr | 0xb8 | R/W | 26 | 19 | ||
ip6po_flags | 0xbc | R/W | 26 | 19 |
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.
Type | Allocatable | Privileges | 0x8 (d) | 0x10 (p) | 0x18 (p) | 0x58 (p) | 0x60 (p) | 0x68 (p) | 0xa8 (p) | 0xb0 (d) | 0xb8 (d) |
---|---|---|---|---|---|---|---|---|---|---|---|
_vector_upl | Yes | None | Doesn'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_registration | Yes | None | Is 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_entry | Yes | None | 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 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_inet | Yes | Root or entitled | Doesn'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_queue | Yes | Root | Could 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_set | Yes | Root | Doesn'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_pager | Hard | None | 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. | Could contain a controlled pointer. | ||||
vm_shared_region | Hard | None | Is 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. | ||||
IOStateReporter | Maybe | Sandbox? | 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. | |||||
IOHistogramReporter | Maybe | Sandbox? | 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. | |||||
IOPowerConnection | Maybe | Sandbox? | 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_t | Maybe | Sandbox? | Is not controllable enough to be a useful pointer. | Is not controllable enough to be a useful pointer. | |||||||
IOCPU | No | ||||||||||
IOCPUInterruptController | No | ||||||||||
IOPMWorkQueue | No | ||||||||||
IOPolledFileIOVars | No | ||||||||||
protosw | No |
Key | Targeting 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.