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