Author: Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team
On the TianfuCup PWN Contest held in November last year, I demonstrated the remote jailbreak of the latest iOS system on iPhoneX. This article is about the Stage 2 of this exploit chain. Here I used a kernel vulnerability that can be directly reached in the sandbox. (I named it Chaos), so after realizing the RCE of Safari, we can trigger this vulnerability directly from the Safari sandbox, thus achieving the remote jailbreak.
In this article, I will release the PoC of Chaos and will elaborate in details (for beginners) how to get the tfp0 exploit details on A12. However, I will not release the exploit code, if you want to jailbreak, you will need to complete the exploit code yourself or wait for the jailbreak community’s release. At the same time, I will not mention the exploit details of the post exploit, as this is handled by the jailbreak community.
This is the rjb DEMO on the latest iPhoneX system that I recorded before the contest:
If you are not a beginner or are not interested in this part, please skip directly.
In Apple's kernel, port is a very important concept which is easy to learn but difficult to be master (especially its reference counting). If you have fully understood what the port is, you are already among the best regarding iOS kernel.
Simply speaking, the port is a one-way transmission channel. The corresponding object in the kernel is ipc_port. There can only be one receiver, but there can be multiple senders. Please remember that it is one-way, not two-way. Because there is only one receiver, so if you want to send a message to a port, you need to have the send right of the port. The right information is stored in the process-related ipc entry table and the right information is independent for each process, even if the port is the same one. For this reason, the permissions of the port can be isolated in each process. However, it should be noted that the kernel object ipc_port is shared. If the kernel port is the same, all ipc entries point to the same port, which also facilitates sharing between port processes.
Port has two important functions. The first is for inter-process communication, the second is for representing a kernel object, equivalent to the handle in Windows. The second is actually the special form of the first one. That is to say, when the receiver of the port is |ipc_space_kernel|, if you want to operate on a kernel object, you need to have the send right of the corresponding port.
The tfp0 is the abbreviation of ‘task for pid 0’. pid 0 corresponds to the kernel process, because task is also a kernel object, so it can be represented by por. If you can get the send right of the task port of pid 0, you can use this task port to call the kernel API of various processes, through which you can read and write arbitrary addresses of the kernel.
In Apple's code, there is one called MIG, which is automatically generated according to the defs file. It usually does some inter-core object conversion (such as from port to kernel object) and object reference count management, and then call the real kernel functions. If the kernel developer is not familiar with the meaning of defs or MIG's management of object reference counts, there is high possibility to manage the reference counts of the kernel objects improperly in the real kernel API of this MIG package, thus causing leaks of the reference counts or double free.
At the very beginning, I saw such a piece of code as below. Please be notified that it’s not the final vulnerability:
We can see that when the semaphore is not empty, each path calls |semaphore_dereference|, except for the path of |!=task|. Hence, my intuition told me that no matter what the MIG code was, there would definitely be a path that can cause the leak of reference counting. After reviewing the MIG function, I found that the path of the |!=task| did have a leak of reference counting. This could be exploited before iOS12 and be started in the sandbox. However, it would take a long time to trigger the overflow of the reference counting, which makes it of little significance. In addition, the bug has been fixed in the latest version.
But if you are an experienced vulnerability researcher, you should have a sharp intuition. As soon as I saw the code I felt that this part of the code is definitely lacking review and the quality is not high enough. After all, the code that can be directly reached in the sandbox, that means the kernel developer may not be familiar with the rules for generating MIG code. This information is more important than finding the bug in the above. So I started to look for these MIG-related kernel functions, of course, sandboxed. This also inspired me on methods of exploiting vulnerabilities later.
Next, I saw a plain kernel function task_swap_mach_voucher in the relevant code, which is the core of that very vulnerability:
Without looking at the MIG function, you will not see the problem with this unimpressive function, so let's take a look at it:
In the function, |convert_port_to_voucher| will increment the corresponding ipc_voucher reference count by one, |ipc_voucher_release | and |convert_voucher_to_port| will decrement the reference count by one. It looks like there is no problem since whether |new_voucher| or |old_voucher| first increment by one and then decrement by one without any assignment, so the reference count does not need to change.
But let's go back and review the unremarkable function, which assigns |new_voucher| to |old_voucher|!!!!! This means that when |task_swap_mach_voucher| is called, |new_voucher| is equal to |old_voucher|. In other words In other words, |new_voucher| will be double free, and |old_voucher| will not have free. At this time, the leaks of reference count occur. So, in all, there are two problems here. Of course, double free is more valuable and does not need to wait a long time to trigger reference counting overflow, so in the end we get the PoC as follows:
First set a dangling pointer with |thread_set_mach_voucher|, then release the ipc voucher object through the vulnerability. Next, trigger the crash with |thread_get_mach_voucher|. Afterwards, I will talk about how to use it on A12.
The UaF vulnerability usually needs to fake the corresponding obejct, so before exploiting this vulnerability, we must figure out first what kind of data structure our UaF object ipc_voucher is.
The good news is that there is an ipc_port_t iv_port in ipc_voucher, and this port can be passed back to user mode via thread_get_mach_voucher => convert_voucher_to_port, which means we can construct a tfp0 directly via fake port. Regarding the use of fake port, I strongly recommend you to read the article written by @s1guza: https://siguza.github.io/v0rtex/. It helps me a lot with my exploitation.
The bad news is that we all know that ipc_voucher is a kernel object, which means that this fake fake port doesn’t have receive right. This has no effect on tfp0, because send right is sufficient in fully controlling this kernel object, but it will affect the exploitation process to some extent.
In the iOS kernel, different kernel objects are isolated in different zones, so that even if the ipc voucher object is released, it is not really released as it will just be in the free list of the corresponding zone. As a result, we can only reassign one Ipc voucher to fill in the area. But, usually, we need to convert the UaF vulnerability to type obfuscation one before exploiting it. That is to say, we need to allocate a different kernel object to fill the freed ipc voucher memory area, here we need to manually trigger the kernel Zone gc to free the corresponding page.
The method I use here is to allocate a lot of ipc_voucher objects, at least more than one page size, and then release all. Because the minimum unit of zone gc is a page, if not all ipc_voucher is released in the page, then Zone gc does not free this page (for details, refer to MacOS X and iOS Internals: To the Apple's Core Page 427 Chinese version):
After the release is completed, we need to release the zone gc and release the corresponding page back to the operating system management. The method of triggering has been introduced in the past exploit from Ian beer, using a large number of ports and sending messages:
Here, I’d like to give you a little tip to avoid a wired senario: You'd better wait a little longer through usleep, because the zone gc takes some time. Unfortunately, because I didn’t wait that long when I debugged, so I reached to a very weird bug: it was running fine in the debugger, but it became “panic” once being out of the debugger. 0x22 Leak a receive port addr
After releasing the corresponding memory, we need to think about what should be filled in. First of all, we must definitely leak some kernel information, such as some heap addresses, because the leaked information needs to be used in faking ipc_voucher and port. We will also need to fill arbitrary data in this memory area for the faking. Hence, OSString first came into my mind as we can completely control the kernel data with OSString and the OSString data can be regained through some API, thus leaking the kernel information.
Regarding the allocation of OSString, we can utilize the ports, |IOSURFACE_GET_VALUE|and |IOSURFACE_SET_VALUE|, of IOSurfaceRootUserClient.
The first information we can leak is a port address, let’s take a look at the code of converting voucher to port:
If iv_port is empty, the kernel will allocate a new port and put it in the iv_port. So the key to reallocate OSString to fake ipc_voucher is to make the offset iv_port empty. After the port allocation is completed, reads the OSString back through API so as to get the newly allocated address of the port.
Another important thing is the offset problem. If the starting position of the allocated OSString cannot just match to the starting position of ipc_voucher, all the data we forged will go wrong. Here, because the size of a page in iPhone XS Max (A12) is 0x4000 and the allocation of the zone is per page, it means that the first ipc_voucher must be page aligned, so that the OSString we allocate only needs to be aligned with the page to ensure alignment with ipc_voucher. This may be a bit difficult to understand due to my expression. Hope you can get a clearer idea by looking at the allocation code below:
The padding will be used later, but, here let’s leave it alone. Because the port is 0x0, so the port will be assign a real address to it, and then locate the port address and the OSString index that occupies the corresponding memory by indexing:
Previously, we mentioned that this port has no send right. Later, we need to use the receive right to leak the slide of the kernel, so I used a trick here by allocating a lot of ports with receive right near it. Finally, we can use this address minus sizeof(ipc_port_t) to get the address of a receive right port.
Because of the SMAP, we need to fake the port in the kernel address. Here we need to get a controllable kernel address, i.e., one of the OSStrings allocated above. We can make this address much easier to speculate by heap spraying a large amount of the allocated memory.
In the begging, I was going to use the leaked port address to calculate the relevant offset to get this address, but then I found that the heap address randomization in iOS is weak, so I used a fixed address here:
Then we reallocate the OSString mentioned above, re-forge ipc_voucher, and make its port point to our controllable address. Remember that we have recorded the corresponding idx of the OSString? Through it we can quickly locate the OSString to be reallocated:
The third parameter here is the address of the port that needs to be forged. We see that there is a magic offset 0x8. If the magic offset is subtracted from the starting position of Fake Voucher, it also points to the second field of padding we mentioned above:
Here I overlapped the memory areas of fake voucher and fake port. In the location of padding+0x8, it is actually the starting address of the fake port, and then it will return to the hash field. This layout can just satisfy the requirements of fake voucher and fake port and avoid “panic” in the same time. The overlap here is really out of no option, because we only have one chance to reallocate. If we reallocate twice, the first allocated OSString is used to fake voucher and the second is used to fake port. Then half of the addresses we guessed may point to fake voucher. The current solution only leads to one possibility, which is, pointing to the fake port.
Since it is necessary to reallocate the data multiple times in the the fake port memory area, it is necessary to find the index of the OSString corresponding to the fake port:
By calling thread_get_mach_voucher=>convert_voucher_to_port, we can get two things we need. The first is the index of the OSString, because convert_voucher_to_port will modify the reference in the fake port area. You can find the index based on this difference:
The second one is the user-mode port pointing to our controllable address, i.e., | fake_port_to_addr_control | in the above figure. Through it and modified data of the fake port, we can do a lot.
By forging a task port in the fake port, and then by calling pid_for_task (there is a lot of discussion on this exploit technique online, so I won't elaborate more it here), we can read arbitrary address, 32 bits each time. The drawback of it is that have to re-allocate the OSString with each reading, because we need to modify the memory address that needs to be read in the fake port. Since we’ve known the corresponding OSString index, we don't need to re-allocate all OSStrings:
Here, rather than only reallocating the corresponding index, I set a range 0x50 which will reallocate the 0x50 OSStrings both before and after this index. To my surprise, this reallocation is amazingly stable.
In the above section, we have leaked a port address with receive right. Using this address plus the arbitrary read, we can finally get the kernel slide. I will not elaborate more on this part , either. Still, reading this article is recommended: https://siguza.github.io/v0rtex/
Now we have to re-allocate the OSString every time we operate the fake port. This is very unfriendly for the exploitation. After getting the slide of the kernel, we should immediately remap the kernel address corresponding to | fake_port_to_addr_control | to our process under the user mode. So each time you modify the data of the fake port, you can modify it directly in the user mode without reassigning the OSString:
After remapping, the corresponding address under the user mode and that under the kernel state can share a physical memory area. Hence, modifying the data of the corresponding address under the kernel mode can be achieved by modifying the address under the user state (unless it is COW).
Since the port's ip_kobject will be checked in convert_port_to_task to see if the address of task_t is equivelant to kernel_task, so we can't directly assign the read kernel_task address to the fake port's ip_kobject, but need it to be memcpy to another kernel address before being assigned.
Here I split it into two steps. Step one, use a real kernel object port to initialize all data of the fake port, because tfp0 and all kernel object ports share the same receiver | ipc_space_kernel |. Here I used a port of IOSurefaceRootUserClient to initialize. If you don't do this, you will get an error when calling the kernel API with tfp0 as many property values have not yet been initialized, such as ip_messages.
Next, copy the original kernel task address to another kernel address, and modify some of the tfp0 port's different parts from the IOSurefaceRootUserClient port:
The last step is to reallocate the port address in fake voucher to make it point to the address of the latest fake tfp0. Then return to user mode via thread_get_mach_voucher to get tfp0 eventually:
Because at the end of the program, there is still a danging Pointer in the thread mach voucher pointing to our OSString. This part of the memory is released when the IOSurfaceRootUserClient is released, i.e., at the end of the process. Besides that, there are a lot of fake ports we forged all pointing to the memory allocated by OSString, so we must clean them up before the end of the process.
Eventually, the tfp0 we generated in the end also needs to be released. So if you want to maintain the persistence of tfp0, it is best to re-construct a new tfp0 in the post exploit phase. Now, the use of tfp0 has completed. Regarding the post exploit, root directory read and write, signature bypass, etc. will not be mentioned here.
We all know that the PAC mitigation was introduced in A12. Many people think that this is the end point of UaF or even jailbreak. It turns out that the UaF hole can still be used in the PAC environment, which depends on the specific situation, because PAC It is only for the indirect call control pc. We can see that in the whole process of getting tfp0, we didn't need to control the pc. This is because there was a port property value in the object ipc_voucher we released. The exploitation of the UaF vulnerability depends greatly on the data structures of the released object, as well as how to use them, since in the end we have to convert to type obfuscation.