
на Mac 10.12.2
На этот раз для повышения привилегий используется уязвимость в mach_voucher_extract_attr_recipe_trap, а основной метод эксплуатации — через сообщения MACH_MSG_OOL_PORTS_DESCRIPTOR.
Если кратко о mach_msg ool: при отправке msg, содержащего ool descriptor, ядро копирует указанные данные из пользовательского пространства в пространство ядра, и ядро хранит эти данные до тех пор, пока целевая задача не обработает сообщение. Аналогично, когда целевой процесс получает сообщение с ool descriptor, ядро копирует данные из пространства ядра в пользовательское пространство (не обязательно реальное копирование). Поэтому эту технику можно использовать для записи данных в кучу ядра или чтения данных из ядра.
Поскольку эксплуатация этой уязвимости значительно сложнее, чем Trident, я буду разбирать её шаг за шагом: от точки возникновения уязвимости до поэтапной эксплуатации. Код можно найти в моём github.
Среди новых функций, добавленных в iOS 10 и macOS 10.12, есть функция под названием mach_voucher_extract_attr_recipe_trap, которая является Mach trap, вызываемым из песочницы. Исходный код этой функции:
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) {
/* сохраняем небольшие рецепты на стеке для скорости */
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;
}
args->recipe_size записывается в sz.sz находится между MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) и MACH_VOUCHER_TRAP_STACK_LIMIT (256), то в соответствии со значением sz выделяется буфер в куче ядра.sz, использованный для выделения кучи, а указатель из пользовательского пространства. Таким образом возникает переполнение кучи, которое мы используем для атаки. Кроме того, функция copyin имеет особенность: при встрече с unmapped страницей она прекращает копирование. Эта особенность будет использована в нашем poc:freelist мы не знаем положение заново выделенных блоков памяти.Сначала нужно разобраться, как ядро обрабатывает MACH_MSG_OOL_PORTS_DESCRIPTOR в mach msg. Когда ядро получает сложное сообщение и обнаруживает ports descriptor, оно передаёт его функции ipc_kmsg_copyin_ool_ports_descriptor (вызываемой из ipc_kmsg_copyin), которая читает все объекты port. Эта функция вызывает kalloc для выделения необходимой памяти (в 64-битной системе выделяемый размер в 2 раза больше входного, длина name — 4 байта), затем преобразует валидные port из name в реальный адрес объекта ipc_port и сохраняет их. Для name, равного MACH_PORT_NULL или MACH_PORT_DEAD, они остаются без изменений.
/* 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;
}
...
}
Поэтому при атаке мы будем отправлять большое количество MACH_PORT_DEAD, заполняя область памяти значением 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD), затем инициируем уязвимость, изменяя один из IPC_PORT_DEAD на заранее подготовленную нами область памяти. Если эта область является валидной структурой ipc port, то после приёма сообщения OOL PORTS мы сможем в пользовательском пространстве получить port name, соответствующий этому ipc_port, и продолжить атаку.
ipc_objectСначала мы получили этот fake port. Для дальнейшей утечки информации необходимо знать, по каким параметрам ядро обрабатывает его по-разному. Рассмотрим структуру ipc_port:
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__));
Один из них — объект ядра, соответствующий порту. Тип объекта ядра, которому соответствует ipc_port, определяется свойствами ipc_object. Таким образом, мы фактически конструируем ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; // устанавливаем как объект IKOT_CLOCK и активируем
fakeport->io_lock_data[12] = 0x11; // устанавливаем блокировку порта активной, чтобы избежать deadlock
Ядро будет воспринимать этот ipc_port как порт для связи с объектом IKOT_CLOCK. Следующая цель — утечка базового адреса ядра.