// ianbeer
针对 iOS 10.2 上 CVE-2017-2370 的内核任意读写利用程序
仅在 iPod Touch 6G 14C92 上测试过——其他设备/固件无法开箱即用!
*** 漏洞 ***
mach_voucher_extract_attr_recipe_trap 是一个可以从任何上下文调用的 mach trap。它是 iOS 10 中新增的代码。
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;
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;
}
以下是参数结构(由用户态控制)
struct mach_voucher_extract_attr_recipe_args { PAD_ARG_(mach_port_name_t, voucher_name); PAD_ARG_(mach_voucher_attr_key_t, key); PAD_ARG_(mach_voucher_attr_raw_recipe_t, recipe); PAD_ARG_(user_addr_t, recipe_size); };
recipe 和 recipe_size 是用户态指针。
在 (a) 处,从用户态指针 recipe_size 读取四个字节到 sz。
在 (b) 处,如果 sz 小于 MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) 且大于 MACH_VOUCHER_TRAP_STACK_LIMIT (256),
则使用 sz 分配一个内核堆缓冲区。
在 (c) 处,再次调用 copyin 将用户态内存复制到刚刚分配的缓冲区中,但传入的大小不是 sz(已校验并据此分配的大小),
而是 args->recipe_size。这是用户态指针指向大小,而不是大小本身!
这导致一个完全受控制的内核堆溢出。注意,这段代码实际上根本无法正常工作 :)
*** 利用 ***
我瞄准的是通过 kalloc 分配的预分配 mach 消息缓冲区。前 4 个字节是大小字段,用于确定 在缓冲区中的什么位置读写消息。通过破坏这个字段,我们可以让 mach 消息在支撑 kmsg 的 kalloc 分配范围之外进行读写。
有一个小麻烦:端口预分配的 kmsg 只会被内核用于实际的 mach_msg 发送(例如不会用于 MIG 方法的回复)。 这使得在其中放入足够多的受控内容变得有点棘手。
内核发送的一种携带大量用户可控数据的 mach 消息是异常消息(exception message),在线程崩溃时发送。
文件 load_regs_and_crash.s 包含 ARM64 汇编,它将 ARM64 通用寄存器加载为某个缓冲区的内容,
这样当线程崩溃时,异常消息会包含该数据缓冲区(大约有 0x70 字节是可控的)。
通过覆盖端口的 ikm_size 字段,使其指向另一个端口的头部,我们可以读写另一个端口的头部,并得知它在内存中的位置。
然后我们可以释放第二个端口,并在其位置重新分配一个 user client,同样可以对该 user client 进行读写。
我读取了 userclient 的 vtable 指针,然后使用 OSSerializer::serialize 小工具技术(详见 [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf]) 以两个受控参数调用任意函数。
我调用 uuid_copy,它会执行 memmove(arg0, arg1, 0x10)。通过将 arg0 或 arg1 指向 userclient 本身(我们可以通过接收
异常消息来读取它),就可以以 16 字节为单位读写任意内核内存。