
Эксплойт Иана Бира для CVE-2017-2370 (чтение/запись памяти ядра на iOS 10.2)
// ianbeer
Эксплойт для произвольного чтения/записи в ядре для CVE-2017-2370 на iOS 10.2
Протестировано только на iPod Touch 6G 14C92 — другие устройства/прошивки не будут работать «из коробки»!
*** баг *** mach_voucher_extract_attr_recipe_trap — это mach-ловушка, которая может быть вызвана из любого контекста. Это совершенно новый код, добавленный в 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. Это указатель на размер из пространства пользователя, а не сам размер!
Это приводит к полностью контролируемому переполнению кучи ядра. Обратите внимание, что код на самом деле не может работать корректно :)
*** эксплойт ***
Я нацеливаюсь на предварительно выделенные буферы mach-сообщений, которые выделяются через kalloc. Первые 4 байта — это поле размера, которое используется для определения того, где в буфере читать и писать сообщение. Повреждая это поле, мы можем заставить mach-сообщения читаться и записываться за пределами выделения kalloc, лежащего в основе kmsg.
Есть небольшое осложнение: предварительно выделенный kmsg порта будет использоваться ядром только для фактических отправок mach_msg (не для ответов на методы MIG, например). Это делает немного сложнее поместить в них достаточное количество контролируемого содержимого.
Один из типов mach-сообщений, которые ядро отправляет с большим количеством контролируемых пользователем данных, — это сообщение об исключении, отправляемое при сбое потока.
Файл load_regs_and_crash.s содержит код ARM64, который загружает регистры общего назначения ARM64 содержимым буфера таким образом, что при сбое сообщение об исключении содержит этот буфер данных (контролируется около 0x70 байт).
Перезаписывая поле ikm_size порта так, чтобы оно указывало на заголовок другого порта, мы можем читать и записывать заголовок другого порта и узнавать его расположение в памяти. Затем мы можем освободить этот второй порт и перераспределить на его месте user client, который мы также можем читать и записывать.
Я читаю указатель vtable user client'а, а затем использую технику OSSerializer::serialize, как подробно описано в [https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf], чтобы вызвать произвольную функцию с двумя контролируемыми аргументами.
Я вызываю uuid_copy, которая вызывает memmove(arg0, arg1, 0x10). Указывая arg0 или arg1 на сам user client (который мы можем прочитать, получив сообщение об исключении), мы можем читать и записывать произвольную память ядра блоками по 16 байт.