From zero to tfp0 - Part 2: A Walkthrough of the voucher_swap exploit
June 1, 2020
In this article, we will get an in-depth look at the voucher_swap vulnerability and all the steps leading up to getting the kernel task port.
All credit for the vulnerability and the PoC goes to @_bazad
The bug in this article is a reference counting issue due to MIG generated code. But let’s understand first what is reference counting ? Reference counting is a form of simple yet effective memory management. It is basically a way to keep a count of the number of references to an object held by other objects. If an object’s reference count reaches zero, the object will be freed. Creating or Copying an object will increase its reference count by 1, whereas destroying a reference or overwriting the object will decrement its reference count by 1. In systems with limited memory, reference counting can prove more efficient than garbage collection (which happens in cycles and can be time consuming) , because objects can be claimed as soon as their reference count becomes zero, and this improves overall responsiveness of the system.
Reference counting can be put on certain objects, with a field in the object struct denoting the reference count. For e.g, the Mach Ports(ipc_port_t) are reference counted objects, with the 32 bit field io_references specifying the number of references, and the functions ip_references and ip_release are used to increase and decrease the reference count on the port. A simple search for ip_reference will give many examples of this function being used to manipulate the reference count of ports.
And same for vouchers, the value iv_refs keeps a track of the reference count, as can be seen in osfmk/ipc/ipc_voucher.c.
The value iv_refs is of the type os_refcnt_t, which is a 32 bit integer, so its range should be from 0-0xffffffff right ? Actually not. The maximum value is defined to be 0x0fffffff (7 f’s) in the file libkern/os/refcnt.c. You may wonder why ? This is a new mitigation to protect against integer overflows, and makes the reference leaks vulnerability unexploitable, but still a reference counting leak vulnerability can let you increase the reference count and hence perform interesting things as we will see later in this article.
Accessing any value out of this range will trigger a kernel panic, as can be seen from the functions below.
The ipc_voucher_release and ipc_voucher_reference functions for a voucher just check whether the voucher is not NULL and call iv_reference and iv_release which then calls os_ref_retain and os_ref_release respectively.
More details can be found under BUILD/obj/EXPORT_HDRS/libkern/os/refcnt.h
There can be two kinds of vulnerabilities that can arise because of this, one is if the reference count can be increased in some way such that it leads to an overflow. We already discussed that because of the maximum cap, this is not really exploitable. However, you can still increase the ref count up to 0x0fffffff and we will use this technique later. The other is lets say the object’s reference count can be set to 0 but there is still a pointer to it. Now, since the reference count becomes 0 the object will be freed, and hence the pointer pointing to it becomes what we call a dangling pointer.
So let’s have a look at the vulnerability. Look under the file /xnu-4903.221.2/osfmk/kern/task.c and the function task_swap_mach_voucher. This is a simple function that is supposed to take a new voucher and an old voucher and swap them. Well, this is what it is suppossed to do but it just removes the old_voucher with the new_voucher.
The function task_swap_mach_voucher is a placeholder as per the comments. A quick search for it would also find the routine under xnu-4903.221.2/osfmk/mach/task.defs
This proves that it is actually a Mach API, since MIG def files are generating code for Mach Interfaces. Lets search for task_swap_mach_voucher. Remember that we are doing this on a compiled version. Under the file /BUILD/obj/RELEASE_X86_64/osfmk/mach/task.h we can find the Mach message format for this function.
And under the file /BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c we can see checks being performed on the request.
And the actual implementation can be found just below it.
Here is the stripped out implementation, with the interesting functions marked in bold.
The function convert_port_to_voucher increases the reference count by one by calling ipc_voucher_reference.
The function convert_voucher_to_port will decrease the reference count by calling ipc_voucher_release.
And within the routine task_swap_mach_voucher, the reference count of new voucher is descreased by one by calling ipc_voucher_release (Line 4844).
Here are the reference count changes.
**Line 4839: Reference count of new_voucher + 1**
**Line 4841: Reference count of old_voucher + 1**
**Line 4843: task_swap_mach_voucher called -> old_voucher = new_voucher**
**Line 4844: Reference count of new_voucher - 1**
**Line 4857: Reference count of new_voucher - 1 (Because old_voucher is now new_voucher)**
I think you are starting to see the problem here. The reference count of new_voucher can be reduced to 0 thereby freeing the object. And the reference count of old_voucher can be increased by too many. As discussed before, the reference count overflow has been protected by the max cap value of 0x0fffffff.
So it is possible to get a dangling pointer pointing to a voucher. This can be done by storing a pointer to the voucher, and then using the vulnerability to reduce the reference count of the voucher to 0, which will free the voucher.
Before proceeding, it is always good idea to look at the object struct and understand the different fields in it.
So the first thing is to identify which object to store the pointer for the freed voucher in. The best way for this is to search for ipc_voucher_t in the kernel source, and look for APIs that easily allow getting and setting of that pointer. One of the places which stands out is in the thread object inside osfmk/kern/thread.h which stores the voucher reference with the name ith_voucher.
The functions thread_get_mach_voucher and thread_set_mach_voucher can be used to read and write the voucher reference from userland. Again, as we recall from part 1, we need to look at the MIG generated code for this function.
Once we get a dangling pointer to a freed voucher object, we can then reallocate the freed voucher object with something else. However, this is not straightforward. Vouchers typically reside in their own zone ipc vouchers as can be seen in osfmk/ipc/ipc_voucher.c where the zinit call allocates a new zone for the vouchers.
So the freed memory for the voucher will be placed in the freelist of the zone and allocated to a new voucher when it is created. Therefore in order to reallocate with some other object, the only feasible way is to initiate zone garbage collection which will move the freed memory for the vouchers (min size is 1 page which includes the freed voucher) into the zone map and then reallocate that memory with something else. Zone garbage collection can be triggered by allocating a large number of vouchers and freeing them, making that memory available for next allocation and then spraying via port pointers as we will see later in this article.
Let’s look closely at thread_get_mach_voucher in MIG generated code again. Assuming we did reallocate the freed voucher with some object, the call thread_get_mach_voucher should succeed without panicking the kernel, since we are interested in tfp0 eventually and not really kernel panics. The function thread_get_mach_voucher inside the kernel which is called on Line 2688 calls ipc_voucher_reference(voucher) , which should mean that the iv_refs field should be valid for the voucher.
Then there is the call to convert_voucher_to_port on Line 2695 which looks like this.
One of the first things which is checked on Line 503 is whether the voucher has a proper ref count. Then on line 507, the voucher’s port is being checked for validity. If it is not valid, a freshly new voucher port is allocated. This is great because while allocating a fake voucher in place of the freed voucher, if we somehow keep the iv_port pointer to be NULL, then we can actually also get a freshly allocated voucher port (IKOT_VOUCHER) for that particular voucher back to userspace, which we can then reference with ith_voucher->iv_port. This will allow us to further manipulate the voucher.
Heap Feng Shu via OOL Ports Descriptor
As discussed briefly in Part 1, complex Mach Messages have a descriptor field, which could be of four types.
MACH_MSG_PORT_DESCRIPTOR: Sending a port in a message
MACH_MSG_OOL_DESCRIPTOR: Sending OOL data in a message
MACH_MSG_OOL_PORTS_DESCRIPTOR: Sending OOL ports array in a message
MACH_MSG_OOL_VOLATILE_DESCRIPTOR: Sending volatile data in a message
When a Mach message is sent with MACH_MSG_OOL_PORTS_DESCRIPTOR, it calls the function ipc_kmsg_copyin_ool_ports_descriptor.
On Line 2879, it calls kalloc to allocate memory in the heap in the kalloc zone and in line 2902, it is substituted as a variable objects which is an array of port pointers. On line 2909, each port is iterated in the descriptor and checked for validity. The function CAST_MACH_NAME_TO_PORT is called on the port which basically does this. If the port is MACH_PORT_DEAD, its filled with 0xFFFFFFFFFFFFFFFF, and if its MACH_PORT_NULL, its filled with 0x0000000000000000.
So basically, by sending a lot of Mach messages with OOL Port Descriptor, it is possible to allocate the kalloc zone with valid pointers, 0xFFFFFFFFFFFFFFFF or 0x0000000000000000. The same memory can be deallocated by receiving the message, and thereby poking holes within the memory. The contents of the received messages will be the ports and they can be analyzed for certain pattern to find overlaps. This technique has been used extensively in previous exploits for performing Heap Feng Shui.
The idea is to send Port pointers in a pattern such that iv_refs is overlapped with lower 32 bits of base port address (Little-Endian system) and its still not more than its max value. Sending base port address at a certain index in the pattern will overlap iv_refs with lower 32 bits and the next field with the upper 32 bits. Hence, incrementing iv_refs will basically increment the base port pointer.
Similarly, overlapping iv_port with MACH_PORT_NULL will be just fine since we can call thread_get_mach_voucher to get a new voucher port that can use to manipulate the reference count again.
In order to allocate the freed voucher with Port pointers, it is essential to initiate zone garbage collection on the ipc vouchers zone. This can be done by allocating a large number of vouchers and then freeing them, essentially making that memory to be used again, the minimum size for which is 1 page, and then spraying the memory with port pointers as described above.
Pipe is another system call in xnu used for IPC. It creates a pipe that allocates a pair of file descriptors and allows unidirectional data flow. The buffer through which the data flows is known as the pipe buffer. Data written to the write end of the pipe buffer can be read from the read end of the buffer, but not vice versa as this feature is not provided by xnu. This basically allows you to read and write into the same address space. The other important thing is that it occupies kva (kernel virtual address) space and hence is a useful primitive for allocating memory in the heap. Another important thing to note is that the pipe buffer size is set to a max value of 16384 bytes by default, and the whole pipe size for all the pipe buffers is set to 16MB.
If the data has been written to the pipe buffer and its full, then the pipe is considered to be blocked. To free that buffer, data must be read out from the pipe buffer. Data can be sprayed using pipe buffers by allocating many pipe buffers and writing data to it. The total number of pipes that can be created is the total pipe size (16 MB) divided by the pipe buffer size (16384 bytes), which is 1024.
The advantage of pipe buffers is that if we are able to get a pointer to one of our pipe buffers and read the value of it, we can basically identify which of those 1024 pipe buffers it is , and then reallocate data in that particular pipe buffer for our benefit.
What we are trying to achieve in this case for the voucher_swap exploit is getting a port pointer to point to one of the pipe buffers, identify which pipe buffer it is, and then reallocating data in that pipe buffer to create a fake port, which can allow us to do certain tasks. Since the Port pointer originally points to a port, if it is possible to somehow increment that port pointer to point to the pipe buffers, that will also work. Hence, you need to spray some ports first such that the ipc.ports zone for the ports grows and fresh pages are allocated from the zone map, then spray the pipe buffers such that the pipe buffers land just in front of the sprayed ports, and then manipulate the port pointer which pointed to one of the sprayed ports incrementally so that it lands into the pipe buffers. In this case, we will use the iv_refs field to point to a port pointer, and then use the vulnerability to leak references thereby increasing it (iv_refs) and pointing it to the pipe buffers.
Now once you receive the messages that you sent for the spray you get ipc_port and send rights to it. However, in this case one of the ipc_port pointer actually points to our pipe buffers. Now we can manipulate that port contents using the read and write functionality of pipe buffers.
So our exploitation steps should look like this.
Create the thread for which the voucher pointers will be kept
Spray the Heap with Ports so that the ipc.ports zone will grow and allocate fresh pages from the zone map. Set the last port as the base_port.
Spray the Pipe buffers and since the memory is freshly allocated, the pipe buffer will land just in front of the ports, since the memory will now be allocated incrementally. The pipe buffers content masks that of a port and each pipe buffers port content has a different IKOT type to identify later which pipe buffer overlaps.
Spray the Vouchers and choose one Voucher to be freed. These vouchers will land in their own zone ipc vouchers
Store a Pointer to the selected voucher that was created in the previous step in the threads ith_voucher field. This will increase its reference count. Now use the vulnerability to reduce the reference count by one again, while still holding a pointer to the voucher
Release the vouchers
Spray using OOL Ports Descriptor by sending mach messages in a pattern (triggering GC) such that iv_refs is overlapped with the base port’s lower 32 bits and the iv_port will be MACH_PORT_NULL. Incrementing iv_refs will basically cross base port and land into the pipe buffers.
Get a new voucher port by calling thread_get_mach_voucher. Now we can manipulate the overlapping freed voucher.
Use reference counting bug to increase the iv_refs and point it to the pipe buffers
Receive the message that was sent using OOL ports descriptor. Look at the ports that were received and find the overlapping pipe buffer by looking at the contents of the port.
Since we can read and write into pipe buffers we can create a fake port in the pipe buffer.
Create a fake IKOT_TASK port and read memory using pid_for_task 4 bytes at a time.
Create a fake Kernel Task Port by copying ipc_space_kernel and kernels vm_map using the read primitives and writing them into the pipe buffers.
Create a better fake Kernel Task Port using mach_vm_allocate
Read and Write Kernel Memory!
Anyways, enough of background, let’s jump into the exploitation in detail.
If you haven’t downloaded it yet, get a copy of the voucher_swap exploit code so you can follow along. In some cases, the comments are self explanatory so i am just gonna skip the explanation.
Step 1: Create a Separate thread for the Voucher
Create a separate thread where we will store the pointer to the voucher. The thread has an ith_voucher field where we can keep the reference to the voucher.
Step 2: Create Pipes for the spray
Generate pipes for the spray. These pipes will be sprayed after the ports spray so they can land in adjacent memory. The maximum size allowed for a pipe buffer is 16384 bytes and the total size for all the pipe buffers is 16MB. Therefore the total number of pipes that can be sprayed is 1024. During the overlap, one of the pipes and its corresponding pipe buffer will overlap with the fake port.
Step 3: Spray the Heap with Ports
We need to spray a lot of IPC ports. Some of these ports will close the existing holes and force the kernel to allocate additional blocks from the zone map. When we spray the pipe buffers after that, we will assume that they land just in front of the ports. The filler_port_count is chosen to be 8000 based on trial and error. The base_port is the last port created using the create_ports call. Remember this as we will use it again in Step 8. The next memory block should be hopefully allocated next to the pipe buffers, and since the pipe size is 16MB, our fake port which we will create inside the pipe buffer should be within the 16MB range. On the first 2000 ports, we also increase the queue limit, which is the maximum of messages that can be sent at once to the port. The reason for doing is on the first 2000 ports is because we will be sending messages using OOL ports descriptor to these ports in order to reallocate the freed vouchers, and hence having the ability to send more messages to these ports would help in the spray.
Step 4: Spray the Pipe buffers
Next, we spray the heap with pipe buffers and hope they land just after the ports in memory.
Also, for each pipe buffer, we are going to write the pipe buffer with possible ipc_port structs and change the 12 bits of IKOT_TYPE for the port to the pipe index. This will help us in finding the overalapping pipe amongst all the pipes, since the data in the pipe buffer will be interpreted as a fake port. This is done by the callback function update which in turn calls iterate_ipc_ports and sets the attributes for the ipc_port. This data is then written to the write end of the buffer.
As we can see from Step 4, the iterate_ipc_ports basically considers the data as ipc_port structs and has a callback function specifying the port offset which is used to set the attributes of the ports.
The callback function is used to update the pipe buffer and overwrite it with ipc_port structs.
Step 5: Spray the Heap with Vouchers
Next, we spray the heap with Vouchers. And also choose one voucher port that will be eventually freed and call it uaf_voucher_port. As discussed in the previous article, memory is taken from the zone map in blocks. The size of a block is fixed for a particular object for a particular version (0x4000 for ipc_voucher for iPhone11,8 16C50). Since the voucher size is also fixed (0x50), the number of voucher objects in a block is also fixed (0x4000/0x50 = 80) The idea is to allocate extra blocks where the voucher that we will free eventually (uaf_voucher_port) will be stored. The first 300 vouchers are basically to fill up the initial holes. And then we spray vouchers to take up about 16 blocks, where we plan to put our freed voucher in the target block (Block 7-10). These blocks will then be used for overlapping with OOL port pointers as we will see later.
Step 6: More Spraying
Next, we spray some more memory using the ports we created earlier. This can be later freed to prompt garbage collection. If you remember we had created filler ports and bumped the queue limit on the first 2000 ports. In this case, the first 500 ports are being used for spraying again.
Step 7: Store a pointer to the voucher but release the reference
The reference is then released by the voucher_release function. Actually, @_bazad created two similar functions for releasing a reference (voucher_release) and leaking a reference (voucher_reference) which are both wrappers over voucher_tweak_references which is a wrapper over task_swap_mach_voucher. As you remember, the vulnerability was in calling the function task_swap_mach_voucher() which takes as input the current task, a new voucher (reference will be released) and and old voucher (reference will be leaked). Hence if you want to release a reference for a voucher, just pass it as an argument instead of the new voucher and the old voucher can be set as MACH_PORT_NULL.
Step 8: Create the OOL ports pattern that will overlap the freed voucher
Now we need to create a pattern of OOL port pointers which will eventually overlap our vouchers. The author chooses the kalloc.32768 zone to overlap the voucher , simply because its 2(BLOCK_SIZE(ipc_voucher))** or **2(0x4000) and hence it will be easier to predict the offsets for the voucher. The number of port pointers are calculated based on the zone size divided by size of uint64_t which is the size of a port pointer. Then calloc call is used to initialize an array with the number of port pointers, each of size mach_port_t and then set to 0. The iterate_ipc_vouchers_via_mach_ports function is used to walk through the port pointers assuming them as vouchers and using a call back function giving out the offset of the voucher, and then setting iv_refs of the voucher to point to the base port, which you must remember from Step 2. The ool_ports[voucher_start + 1] is used because the iv_refs is at an offset 0x8 from the start of the voucher, and hence ool_ports[voucher_start + 1] will actually point to index 0x8 of the voucher. We will make the iv_refs field point to the base port, which is just before the pipe buffers. We also leave the iv_port pointer as MACH_PORT_NULL (set by calloc), so that when we can call thread_get_mach_voucher later on we get a new voucher port.
Step 9: Free the first GC Spray
Step 10: Release the Vouchers created earlier thereby leaving a dangling port
Step 11: Release the Vouchers to overlap with the port pointers
If you remember from Step 6, we used 500 (gc_port_count) of the 2000 ports that we had bumped the queue limit to already for spraying. So now we will spray the other ports until we hit the total spray size as 17% of our platform size. The ool_holding_ports pointer is taken from index 500 (gc_port_count) onwards since we already used the first 500 for spraying. The idea is to also keep allocation size as 32768 so that it lands in the kalloc.32768 zone, this is done by keeping the number of port pointers for each message (ool_port_count = ool_port_spray_kalloc_zone / sizeof(uint64_t), where ool_port_spray_kalloc_zone = 32768), and hopefully after this the memory freed earlier from the vouchers will be reallocated here.
If you look under the method ool_ports_spray_size_with_gc, there is also delay added between every 2MB (gc_step) of spray with usleep() to give time for zone garbage collection.
Each of these ports are sprayed using mach messages with OOL port descriptors. This will allocate kernel memory and fill them with port pointers. The following code in ool_ports_spray_port is used to allocate parameters and send the message.
Step 12: Call thread_get_mach_voucher() to get a voucher port for the freed voucher
Using thread_get_mach_voucher, we can recover the voucher port for the freed voucher, and this will allow us to further manipulate the reference count of the voucher.
Step 13: Modify the iv_refs to point to pipe buffers
Using the voucher port, we can modify the iv_refs value using the same vulnerability (reference leak this time) and hope that it points to our pipe buffers. If you recall from before, the iv_refs was actually pointing to the base port. So now the iv_refs pointer is incremented by 4MB (base_port_to_fake_port_offset) in this case, and if you remember we sprayed about 16MB of Pipe buffers, so the Port pointer should overlap somewhere within our sprayed Pipe buffers.
Step 14: Identify Voucher Port and overlapping fake port
Now since the freed voucher (which is actually overlapped with port pointers) has an iv_refs pointer pointing to somewhere within the pipe buffers, we need to find out which of the 1024 pipe buffers is it. In order to do that, we receive the messages that we sent earlier using OOL Ports descriptor. We loop through all the descriptors in the message and pass them to a handler block with the parameter as the starting ports address and the total number of ports. Then we loop through each of these port pointers as vouchers using a helper function iterate_ipc_vouchers_via_mach_ports that gives out address of all possible vouchers by dividing the size of all port pointers by voucher size. The ool_voucher_port can be identified because it will have a valid voucher port, since we called thread_get_mach_voucher() only on that voucher, and also by checking against uaf_voucher_port at an offset of 7 when looping as port pointers, since its 7*8 which is 56 bytes (offset of iv_port) in the voucher struct. The fake port is identified simply as the value pointing to the iv_refs which is at an offset of 0x8 and hence index 1 when using port pointers.
Step 15: Find overlapping pipefds
Next, we need to identify that out of the all the pipe buffers that we created, which one overlaps with the fake port. To do that, we use the API mach_port_kobject to get the IKOT_TYPE value of the fake_port and this value should be the index of the pipe, because if you remember, in Step 4, we were creating ports within the pipe buffers and for each port that we created, we were overalapping the IKOT_TYPE with the index of the pipe buffer. Using this, we can identify which pipefds our fake port is overlapping with.
Step 16: Clean up the unused memory
Step 17: Set up primitive to find the address of the base port
We have a fake port overlapping with the content of the pipe buffer, that we can read and write into since we know which pipe buffer is it. Now our task is to create a fake port such that we can use the pid_for_task() technique with it to read 4 bytes of kernel memory at a time. This technique was discussed in the Part 1 of this article.
But what this also means is that our fake task’s kobject field should point to a task struct that we control, so that we can have a look at the bsd_info field of the task that points to a proc struct. Ideally, the fake port along with the fake task should both be in the pipe buffers, so we can read and write into them. In order to find that out, we send the mach api call mach_port_request_notification() to the fake port to add a request that if the fake port becomes a dead name (MACH_PORT_DEAD), the base port will be notified. This causes our fake port’s ip_requests field to point to an array that contains a pointer to the base_port address.
Step 18: Find the address of the base port
We read from the overlapping pipe buffer and iterate though the whole buffer as ports, look at each possible port’s ip_requests field, and if we find that field, we know that it contains the address of an array that contains a pointer to base_port, because this is the only port we have set a notification for. Note that we still can’t read that address yet. We save the offset of that fake port within the pipe buffer. Then we write to the pipe so the data from the pipe can now be read later on. We now know exactly at what offset the fake port lies in the pipe buffer and within which pipe buffer it lies (we already found that out before). We also know the address of ip_requests so we need a way to read from that address.
Step 19: Find the address of the base port
We can find the address of the base port pointer since its at a fixed offset from the ip_requests field. Next, we need to find out the address of the base port from the base port pointer using which we can locate our pipe buffer address. However, as discussed a bit earlier, in order to create a proper fake port on which you can use task_for_pid() on, you must have a kobject field pointing to an address that corresponds to a task. Also, the task will have a bsd_info pointing to a proc. This is achieved by creating a fake port of type IKOT_NONE, creating a fake task and setting the bsd_info field pointing to the (AddressToRead - OFFSET(pidInProcStruct)), and then sending that fask task in a mach message to the fake port. By looking at the port’s ip_messages.imq_messages field via the pipe we can get the address of the ipc_kmsg struct containing the task address, and then replace the port to an IKOT_TASK port with the kobject field pointing tot the fake task. Now that we have built an initial read primitive, we can then use the function stage0_read64 to read the base_port address.
The stage0_read is a really handy function and basically does the job of reading out the kernel memory 32 bits at a time. It basically does the following steps.
Create a fake port in the pipe, set all the required properties and set the IKOT type as IKOT_NONE
Create a fake task, set the bsd_info field depending on the address you want to read and send it to the port in a mach message.
Read the receiver port contents by reading the pipe and finds the address of the task from its imq_messages field.
Rewrite the port by rewriting the pipe and now set the IKOT type as IKOT_TASK to create it as a fake task port so one can use the task_for_pid() call on it
Call pid_for_task to read kernel memory
Step 20: Compute the address of the fake port
Since we know the base_port address and given the fact that we know the offset from the base port to the fake port (we defined this earlier in Step 3), it is possible for us to calculate the fake port address.
Step 21: Compute the address of your own task port
Now that we know the address of the fake task and we can create the port, we can create a better read primitive and call it stage 1. The next step is to compute the address of your own task port. The function stage1_find_port_address takes the input as a task and gets the address of the task port using the stage 1 read primtive.
Step 22: Get the address of the Host port
We need to get the host port address first using which we can find the ipc_space_kernel in later steps. In order to achieve a full kernel read/write, we need to find kernel vm_map and the kernel ipc_space. Since the ipc_space_kernel can be identified using the host port’s receiver field, it is essential to find the address of the host port.
Step 23: Get ipc_space_kernel from the host port’s ip_receiver
Recall from Part 1 that the ipc_port struct has a receiver field which points to the ipc_space. We can read the ipc_space_kernel by reading the host ports ip_receiver field.
Step 24: Get the address of the kernel task port
The next step is to find the kernel vm_map, and to do that we can first find the kernel task port and from there onwards get the vm_map at a fixed offset. In the heap, the kernel task port would be near to the host port, so therefore we can iterate into that particular block as task ports and identify the kernel task port and subsequently get the kernel vm_map.
The following function checks whether a port is a kernel task port or not. It first looks up the bits field to see if it is of type IKOT_TASK to identify whether it is a task port. It then reads the address pointed to by the kobject field which is the corresponding task, looks up the bsd_info field in that task to find the proc structure it is pointing to, and then reads the pid value. If it is 0 this means it is the kernel task port.
Step 25: Get the address of the vm_map
Now that we have identified the kernel task port, we can read the vm_map since it is at a fixed offset.
Step 26: Create a fake kernel task port
Now we can build a fake kernel task port, all of which is still within the pipe buffer.
The criteria for a fake kernel task port is that the fake task’s map field should point to the kernel vm_map and the receiver field should point to the ipc_space_kernel. This is acheived with the following 2 lines.
Step 27: Create a fake kernel task port
Now that we have a fully functioning kernel task port and we can call the Mach APIs to read and write memory, it is time to build a more stable kernel task port. This time, memory is allocated via mach_vm_allocate and the kernel task port may be created even outside the pipe buffer.
Step 28: Clean up the unneeded resources
Step 29: Clean up some more unneeded resources and now we have a stable tfp0
All set, now we have acheived full kernel read/write.
In this article, we looked at the voucher_swap() vulnerability discovered by @_bazad and explained the steps leading up to obtain tfp0 in iOS 12. In the next article, we will look at the Undecimus jailbreak and all the steps needed to successfully jailbreak an iOS device.
Project Zero Issue tracker - https://bugs.chromium.org/p/project-zero/issues/detail?id=1731