Posted by Ian Beer, Project Zero (2021-02-04)
Disclosure or Patch Date: 5 November 2020
Product: Apple iOS
Affected Versions: iOS 14.1 and previous
First Patched Version: iOS 14.2
Issue/Bug Report: https://bugs.chromium.org/p/project-zero/issues/detail?id=2107
Patch CL: N/A
Bug-Introducing CL: N/A
Exploit Sample: N/A
Access to the exploit sample? Yes
Reporter(s): Ian Beer of Google Project Zero
Bug Class: Type Confusion
Vulnerability Details: A kernel type confusion between an ipc_port pointer and a host_notify_entry pointer due to failure to account for the semantics of IKOT_HOST_NOTIFY ports in turnstile code.
IKOT_HOST_NOTIFY ports have slightly unusual semantics: the host_request_notification method allows userspace to take a regular, userspace-owned mach port and set the kobject field to point to a host_notify_entry object. The kobject field is actually a member of a fairly large union (kdata) leading to plenty of opportunities for type confusions if kernel code doesn't account for the semantics of IKOT_HOST_NOTIFY ports (i.e., if the kernel code doesn't know that userspace-owned ports can suddenly become IKOT_HOST_NOTIFY ports.)
struct knote *sync_inheritor_knote;
struct turnstile *sync_inheritor_ts;
There have been type confusions here before; for example a type confusion between imp_task and kobject (via the IKOT_HOST_NOTIFY trick) was fixed in MacOS 10.10.
In this case the turnstiles code added around iOS 12 added the sync_inheritor_port field to the kdata union to indicate the destination port to which a new type of port called a special reply port had been sent. Through some mach port gymnastics it was possible to get the kernel to read a pointer to a host_notify_entry as a ipc_port pointer (via sync_inheritor_port) and then cause an out of bounds write.
Specifically: send a mach message to a destination port and attach the thread_special_reply_port as the msgh_local_port with a SEND_ONCE disposition.
The trick to make something bad happen is to set the MACH_SEND_SYNC_OVERRIDE flag when sending that message. This allows you to change the ip_sync_link_state value away from PORT_SYNC_LINK_ANY.
After sending that message, convert the thread's special_reply_port to a IKOT_HOST_NOTIFY via host_request_notification.
Then attempt to receive a message on that special_reply_port and you'll hit the type-confusion when ipc_port_adjust_special_reply_port_locked reads special_reply_port->ip_sync_inheritor_port expecting a valid port pointer, but actually finds a host_notify_entry pointer because the special_reply_port was converted to a host_notify port.
Is the exploit method known? Analysis is incomplete at this time.
Exploit method: There are some neat tricks to turn this into an arbitrary read/write; I haven't completed that analysis phase yet.
How do you think you would have found this bug? This bug could be seen as a variant of earlier issues involving this union; it could have been found via manual review if you were familiar with those earlier issues. Actually figuring out the conditions required to cause an exploitable type confusion are non-trivial, and I wouldn't expect a fuzzer to find them.
(Historical/present/future) context of bug:
Areas/approach for variant analysis: I would suggest auditing similar uses of complex unions.
Found variants: N/A
Structural improvements: The use of a union here seems unnecessary; the memory saving is negligible on modern systems. My suggestion would be to either add another memory to ipc_port which stores the current valid field of the kdata union, or break the union fields out into separate members.
Potential detection methods for similar 0-days: