/oob_timestamp

copy of original for audit. will be remove soon

Primary LanguageC

oob_timestamp - Exploit for CVE-2020-3837 (P0 issue 1986) on iPhone12,3 17C54
===================================================================================================
Brandon Azad


Issue 1986: iOS/macOS: OOB timestamp write in IOAccelCommandQueue2::processSegmentKernelCommand()
---------------------------------------------------------------------------------------------------

While investigating possible shared memory issues in
AGXCommandQueue::processSegmentKernelCommand(), I noticed that the size checks used to parse the
IOAccelKernelCommand in IOAccelCommandQueue2::processSegmentKernelCommand() are incorrect. The
IOAccelKernelCommand contains an 8-byte header consisting of a command type and size, followed by
structured data specific to the type of command. When verifying that the size of the
IOAccelKernelCommand has enough data for the specific command type, it appears that the check
excludes the size of the 8-byte header, meaning that processSegmentKernelCommand() will parse up to
8 bytes of out-of-bounds data.

Normally I wouldn't consider this very security-relevant. However, command type 2 corresponds to
kIOAccelKernelCommandCollectTimeStamp, which actually *writes* into the OOB memory rather than just
parsing data from it. (The IOAccelKernelCommand is being parsed from shared memory, so the write is
visible to userspace.) This makes it possible to overwrite the first 1-8 bytes of the subsequent
page of memory with timestamp data.

The attached POC should trigger the issue on iOS 13. Tested on iPod9,1 17B111. I haven't tested on
macOS, but it looks like the issue is present there as well.

I'll also tack on to this issue that on the whole AGXCommandQueue seems to do a poor job of
treating shared memory as volatile, and I suspect that there are further issues here that are worth
looking into. For example, when IOAccelKernelCommand's type is 0x10000,
AGXCommandQueue::processSegmentKernelCommand() does not use the fourth parameter (which points to
the end of the IOAccelKernelCommand as parsed by
IOAccelCommandQueue2::processSegmentKernelCommands()) except when passing it to
IOAccelCommandQueue2::processSegmentKernelCommand(), instead double-fetching the command size from
shared memory to verify that all the command data is in-bounds. Thus, I believe it's possible to
make AGXCommandQueue::processSegmentKernelCommand() parse out-of-bounds data, although I have not
found a way to turn this into an interesting exploitation primitive. I don't think the shared
memory issues are isolated to this function either. For example, there used to be much more readily
exploitable double-fetches in AGXAllocationList2::initWithSharedResourceList(), although these were
fixed sometime between 16A5288q and 16G77.


Exploit tuning
---------------------------------------------------------------------------------------------------

As explained below, the exploit is suitable for use as a research tool, but in its current form
requires profiling your device's memory layout to select an appropriate kernel pointer value. I
explain how to do so here.

Before you begin, close all apps on your device and reboot it. Define
PROFILE_COMMAND_BUFFER_ADDRESS to 1 in the file oob_timestamp.c. Enable Airplane mode and reboot
your device again.

Unlock the device, leave it sitting idle for about 30 seconds, and then run oob_timestamp from
Xcode; the device should immediately panic. Repeat this about eight times.

Once you have collected the panic logs, find the fault address of each attempt in the FAR register.
To compute the profiled address, take the average of the minimum and maximum of these values, round
down to the nearest 16K page, and subtract 48 MB. Set ADDRESS(fake_port_page) to the resulting
address and set PROFILE_COMMAND_BUFFER_ADDRESS back to 0. You can now exploit your device.

The process of exploiting the device requires the same initial setup (booting the device with
Airplane mode enabled and running the exploit after 30 seconds of idle) to ensure that the kernel
memory layout is consistent with how it was profiled.


Exploitation
---------------------------------------------------------------------------------------------------

The vulnerability allows us to write part or all of an 8-byte timestamp past the end of a shared
memory buffer mapped in a pageable submap of the kernel map. Such pageable maps are allocated
as-needed by IOIteratePageableMaps(). Since the overflow is not occurring inside the zone map
(where most kalloc allocations take place), we need a slightly different technique than has been
used in the past few public iOS kernel exploits.

From a high level, the idea is to use the out-of-bounds timestamp write to corrupt the size of an
ipc_kmsg so that when the message is destroyed it will free a subsequent out-of-line ports array as
well, allowing us to reallocate the out-of-line ports array with controlled data and receive a fake
Mach port back in userspace. The ipc_kmsg structure is convenient for this exploit because (1) the
ipc_kmsg's size is stored as the first field and (2) we can control the size of ipc_kmsg allocated.

Since we will need an out-of-line ports array to land directly after the ipc_kmsg, and since the
maximum size of an out-of-line ports array is 8 pages (16382 ipc_port pointers), we will work with
minimum 8-page object sizes. Doing so allows us to fragment the kernel map early on so that
allocations of size less than 8 pages won't interfere with our "heap" manipulation.

We can use the ability to control how many bytes of the timestamp land out-of-bounds to bound the
resulting value of ipc_kmsg->ikm_size to within the range [0x0003ffa9, 0x0400a8ff]. Then, we spray
80 MB of data allocated using kmem_alloc() without the KMA_ATOMIC flag directly after the
out-of-line ports, creating a buffer region larger than the maximum corrupted size of the ipc_kmsg.
We need to spray this special type of memory allocation because the ipc_kmsg's new end might land
in the middle of a sprayed data buffer, meaning that the buffer is split when the ipc_kmsg is
passed to kfree(). This spray thus ensures that we don't try to free a hole or split an atomic
entry, both of which would panic.

Once the ipc_kmsg's size has been corrupted, we can free it (and the out-of-line ports and some of
the buffer allocations) by destroying the Mach port on which it is queued. This leaves the Mach
message containing the out-of-line ports descriptor pointing to free memory.

We reallocate that memory with another spray of kmem_alloc() allocations without the KMA_ATOMIC
flag, since receiving the out-of-line ports will split that allocation. The data we spray will
contain a pointer to a fake ipc_port inside the shared memory region at an offset that overlaps
with an ipc_port pointer in the out-of-line ports array.

Normally, we would first leverage the overlapping objects to disclose a kernel pointer, so that we
could reliably deduce the address of the shared memory region and thus put a known-good fake
ipc_port pointer in the reallocated out-of-line ports array. However, I have deliberately chosen
not to do so here, and instead I rely on a hardcoded address that tends to be mapped on my
particular device. The result is that this exploit will need to be tuned to a particular device
(including the hardware model, operating system version, and even what apps are running before the
exploit is launched) before it will work. This choice was made to minimize the chance of
opportunistic reuse and weaponization of this exploit by unskilled attackers to target unpatched
users. As released, the fake port pointer I've chosen works on my factory-reset iPhone12,3 64GB
17C54 with no additional apps installed and no apps running before the exploit, and succeeds
upwards of 75% of the time. With tuning, you should be able to achieve similar results on your own
research device. See oob_timestamp.c for details.

Even though I am releasing this exploit in a deliberately broken form, it is fully possible to
rework the exploit to remove this restriction and make it work reliably across a broad range of
devices. The fact that this particular exploit does not work generically does not mean that
attackers in general are unable to exploit this vulnerability. I may release a more complete
version of this exploit at a later date.

The fake ipc_port pointer value in the reallocated out-of-line ports array is chosen to point to a
fake ipc_port in the shared memory region out of which we overflowed earlier. On receiving the
Mach message containing the out-of-line ports descriptor, we obtain a receive right to the fake
port in shared memory, so we can directly read and write the ipc_port's fields from userspace. It
is then possible to convert the fake ipc_port into a fake task port and read kernel memory using
the traditional pid_for_task() approach.

Once we can read kernel memory, we locate kernel_map and ipc_space_kernel and transform our fake
ipc_port into a fake kernel task port. This allows us to use APIs like mach_vm_read_overwrite(),
mach_vm_write(), mach_vm_allocate(), etc. on the kernel to manipulate kernel memory. We build a new
kernel task port in freshly allocated memory and modify the ipc_entry for the fake port to point to
the new fake kernel task port instead. We patch up the OSData objects we used to spray kmem_alloc()
allocations to prevent the system from panicking when they are freed.

Finally, we persist the kernel task port as host special port 4 and store the kernel base address
in TASK_DYLD_INFO all_image_info_addr.


---------------------------------------------------------------------------------------------------
Brandon Azad