@Peterpan0927 wrote:
0x00.前言
感觉名字不好听,但是我也不知道取什么名字了…
在玄武实验室的日推中发现了这个漏洞,发现又是个没见过的
bypass
姿势,于是就来研究一下吧,这个是利用了launchd
的一个漏洞,通过向他发送恶意消息可以将对应的进程dealloc
掉,然后伪造这个进程,相当于做port
间的中间人的攻击,就可以拿到其他进程的send right
,在这一点的基础上进行沙盒逃逸,提权和绕过签名。通过Brandon的写的文章我们来对整个的利用过程进行一个剖析,其实也可以视为是对他的文章的翻译,稍微修改了下,因为原文已经说的算是比较清楚了。
0x01.漏洞产生处
Brandon在进行iOS上的crash报告研究的时候,发现了这个漏洞,可以一种特殊的
crash
方式,可以让内核向launchd
发送一个Mach message
,从而使launchd
将这个进程的send right
在他的ipc_space
中over-dealloced
掉(double free)。那么我们就可以冒充这个进程。这个漏洞在
macOS
上也出现了,只不过在iOS上触发条件更为严格,因为在iOS上要求这个Mach message
从内核发送。launchd在处理EXC_CRASH异常消息时的over-deallocation
当一个进程发送
mach_exception_raise
或者mach_exception_raise_state_identity
消息给他的bootstap port
的时候,launchd
将会把这个异常消息作为一个host level
的异常去接收。不幸的是,
launchd
去处理这些代码的方式是有问题的,当异常的类型是EXC_CRASH
的时候,launchd
会销毁掉消息中的thread
和task port
并返回KERN_FAILURE
,接下来MIG系统会把这些再次销毁(这样的原因是因为如果返回的是KERN_SUCCESS
,就意味着launchd持有着这个消息中的资源,如果是KERN_FAILURE
,就意味着它并没有这些资源的所有权)下面就是处理部分的代码:
kern_return_t __fastcall catch_mach_exception_raise( // (a) The service routine is mach_port_t exception_port, // called with values directly mach_port_t thread, // from the Mach message mach_port_t task, // sent by the client. The exception_type_t exception, // thread and task ports could mach_exception_data_t code, // be arbitrary send rights. mach_msg_type_number_t codeCnt) { __int64 __stack_guard; // ST28_8@1 kern_return_t kr; // w0@1 MAPDST kern_return_t result; // w0@4 __int64 codes_left; // x25@6 mach_exception_data_type_t code_value; // t1@7 int pid; // [xsp+34h] [xbp-44Ch]@1 char codes_str[1024]; // [xsp+38h] [xbp-448h]@7 __stack_guard = *__stack_chk_guard_ptr; pid = -1; kr = pid_for_task(task, &pid); if ( kr ) { _os_assumes_log(kr); _os_avoid_tail_call(); } if ( current_audit_token.val[5] ) // (b) 如果发送这个消息的进程pid不是0 { // (不是内核进程) result = KERN_FAILURE; // 那么就会被拒绝 } else { if ( codeCnt ) { codes_left = codeCnt; do { code_value = *code; ++code; __snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value); --codes_left; } while ( codes_left ); } launchd_log_2( 0LL, 3LL, "Host-level exception raised: pid = %d, thread = 0x%x, " "exception type = 0x%x, codes = { %s }", pid, thread, exception, codes_str); kr = deallocate_port(thread); // (c) 消息中的"thread" port if ( kr ) // 被deallocate掉了 { _os_assumes_log(kr); _os_avoid_tail_call(); } kr = deallocate_port(task); // (d) 消息中的"task" port if ( kr ) // 被deallocat掉了 { _os_assumes_log(kr); _os_avoid_tail_call(); } if ( exception == EXC_CRASH ) // (e) 如果异常的类型是 result = KERN_FAILURE; // EXC_CRASH, 就会返回 else // KERN_FAILURE,MIG result = 0; // 就会再次deallocate这些port } *__stack_chk_guard_ptr; return result; }
要想真正利用这个漏洞,就要能控制我们想要释放的服务,然后伪装成这个服务,那么我们就有很多的机会去提权呢,那么如何做到精准的释放呢?
触发漏洞
我们之所以能够触发漏洞来精准的释放我们想要释放的服务来源于
task_set_special_port
,在内核生成一个task
的异常消息的时候,内核会使用task_set_special_port
的send right
,而不是task
本身的,所以同理,通过thread_set_special_port
这个API就能达到我们的目的了。总的来说,我们分为下面几步:
- 通过
thread_set_exception_ports
来将launchd
作为异常处理者- 通过
bootstrap_look_up
来找到我们想要伪装的服务- 通过
task_set_special_port
/thread_set_special_port
设置将要替代的服务,用于替代异常消息中的send right
- 调用
abort
,内核就会生成EXC_CRASH
类型的异常消息发送给launchd
launchd
解析异常消息释放掉目标服务在crash之后继续运行
因为调用
abort
之后我们的进程就会被杀掉了,我们想要继续运行接下来的代码就需要新的方法如果是其他的异常类型进程是可以恢复的,只需要将其
thread exception handler
设置为launchd
,而task
级别的设置为他自己。那么在launchd
无法处理这个异常的时候,就会交给它自身了,从而线程状态并告知内核异常消息已经被处理。但是一个进程不能捕捉到它自身的EXC_CRASH
消息,所以我们需要两个进程。一个策略就是首先在另一个进程中触发漏洞,强制设置
kernel port
并crash
掉,然而,用App extension
是一个更好的方式。
App extension
在iOS 8中引入,它提供了将应用的一些功能打包,运行在应用之外的能力,它的代码运行在一个隔离的沙盒进程中,本来是和App extension
通信的API,但是Ian McDowell写了一个文章描述如何通过私有APINSExtension
去启动应用扩展并和它通信,我们也就是通过向launchd
注册应用扩展服务的那个端口和应用扩展进程之间通信。避免launchd中的端口复用
这里就是说了一个老生常谈的技巧,为了防止端口被其他的服务给抢占了,我们可以注册大量的服务,持有这些端口的
recv right
,那么等我们abort
的时候,这些端口也被释放掉了,构造出一长串的freelist
,而且我们最先释放的就是我们的目标服务,所以之后注册的服务就不大可能会复用到它头上来了。这个方法的局限性就在于我们需要
com.apple.security.application-groups
的entitlement
去向launchd
注册服务,虽然还有其他方式,但这种毫无疑问是最简单的了。伪装成被释放的服务
在我们的应用扩展释放了
launchd
中的目标服务的send right
,我们需要占有那个port name
,从而可以做port
之间的中间人攻击,截获所有客户端和service
通信的消息。这里因为已经使用了应用组的
entitlement
,所以我们就注册大量的服务直到他们其中的一个重用到了之前的那个port name
,那么其他的客户端寻找目标服务的时候launchd
就会将客户端的send right
返回给我们的端口,而不是原先的服务。0x02.攻击步骤
源代码都在
sandbox_escape.c
中,感兴趣的可以去参考链接中下载继续分析一下。步骤1.获取
host-priv
端口我们的目标就是伪造
SafetyNet
,然后使ReportCrash
崩溃掉,然后从异常消息中取回ReportCrash
的task port
,然后通过task_get_special_port
拿到host-priv port
,这就是我们整个流程的思路。ReportCrash和SafetyNet
ReportCrash
是在iOS系统上生成崩溃报告的,它事实上有4个服务,每一个都在不同的进程中:
com.apple.ReportCrash
,它是EXC_CRASH
、EXC_GUARD
和EXC_RESOURCE
在host level
的处理者com.apple.ReportCrash.Jetsam
处理Jetsam
的报告com.apple.ReportCrash.SimulateCrash
创建模拟器的崩溃报告com.apple.ReportCrash.SafetyNet
是com.apple.ReportCrash
的异常处理服务当
ReportCrash
启动的时候,它会在launchd
中去寻找SafetyNet
服务,并将返回的端口作为task level
的异常处理,也就是说,当ReportCrash
崩溃的时候,由SafetyNet
去处理它的消息,不仅如此,这两个服务在沙盒中都是可以访问到的。操作ReportCrash的前提
要想引出接下来的攻击,我们必须要达成接下来的步骤:后台
ReportCrash
,然后强迫它退出,奔溃掉,并保证我们使用它的时候它是一直运行的,至于为什么这样做,怎么做到接下来就是解释部分了:启动部分很简单,只需要通过一条
Mach message
,launchd
收到请求就会在启动他了,然而由于它奇怪的设定,除了mach_exception_raise_state_identity
之外的任何消息都是使它停止接收新消息并退出,如果我们之后要让它一直存活就要注意这一点。退出很简单就不说了,崩溃有很多方式,最简单的就是发送一个
thread port
设置为MACH_PORT_NULL
的mach_exception_raise_state_identity
消息即可。要保持让它一直运行,而且我们只能发送
mach_exception_raise_state_identity
消息,所以我们只能从这个消息上去想办法,ReportCrash
只有当所有生成崩溃报告的线程完成之后才会退出,所以我们只要想办法阻塞其中一个线程即可从函数的调用可以发现当
ReportCrash
想要创建一个崩溃报告的时候,会通过task_policy_get
方法从异常消息中获取task port
,这会向那个端口发送一个消息并等待回复,而我们的这个task port
可以自己设置,从而让它一直等待回复,而ReportCrash
则一直等待task_policy_get
这个函数去返回。下面解释为什么要这么做:
- 我们要伪造的服务是
SafetyNet
,通过漏洞将它释放掉然后我们自己占有原来的那个port name
- 让所有的
ReportCrash
实例退出掉,来确保接下来的ReportCrash
会去查找我们伪造的服务,并将其作为EXC_CRASH
的接收目标- 崩溃
ReportCrash
,我们伪造的服务将接收到崩溃消息- 从消息中可以提取到
ReportCrash
的task port
- 通过
task_get_special_port
拿到host port
,因为这个是以root
身份运行的,所以就是一个host priv
端口步骤2.沙盒逃逸
虽然拿到了
host priv
端口,但是我们还没有在沙盒之中,所以我们还需要进行沙盒逃逸,严格的来说这两步并不存在先后顺序,只是沙盒逃逸会让系统变得不稳定,所以我们就先拿到host priv
端口再说。这一步中我们还是利用
launchd
的漏洞去拿到task port
,伪造的服务是CARenderServer
,然后和com.apple.DragUI.druid.source
通信,druid
是一个无沙盒的守护进程,会将它的task port
通过Mach message
传给我们伪造的服务。但是这个方式在
iOS11.3
之后就不能用了,但是可以去寻找其他符合的服务,但前提是我们能够伪造成系统的服务,不然就是一切就休,不用谈下一步了崩溃druid
就像之前对
ReportCrash
所做的事情一样,这里用到了一个libxpc
的bug去达成,作者发现了一个可以让任何XPC
服务崩溃掉的越界读:void _xpc_dictionary_apply_wire_f ( OS_xpc_dictionary *xdict, OS_xpc_serializer *xserializer, const void *context, bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *) ) { ... uint64_t count = (unsigned int)*serialized_dict_count; if ( count ) { uint64_t depth = xserializer->depth; uint64_t index = 0; do { const char *key = _xpc_serializer_read(xserializer, 0, 0, 0); size_t keylen = strlen(key); _xpc_serializer_advance(xserializer, keylen + 1); if ( !applier_fn(key, xserializer, context) ) break; xserializer->depth = depth; ++index; } while ( index < count ); } ... }
很显然的看出来上面的
strlen
函数没有对用户的数据做检查,所以在反序列化的时候访问越界内存或者_xpc_serializer_advance
尝试找到data的末尾都会导致crash。所以我们只需要构造一个键值没有闭合的字典作为XPC消息就可以让
druid
crash了。获取druid的task port
- 通过
launchd
的漏洞伪造CARenderServer
- 通过
Mach message
启动druid
- 如果没有收到
task port
就用libxpc
的bug杀掉再重启- 拿到
druid
的task port
绕过平台二进制
task port
的限制虽然我们拿到了
druid
的task port
,但是并不能做到在这个进程内的代码执行,原因就是因为task_conversion_eval
,在源码中可以看到调用关系:task_t convert_port_to_task( ipc_port_t port) { return convert_port_to_task_with_exec_token(port, NULL); } task_t convert_port_to_task_with_exec_token( ipc_port_t port, uint32_t *exec_token) { task_t task = TASK_NULL; if (IP_VALID(port)) { ip_lock(port); if ( ip_active(port) && ip_kotype(port) == IKOT_TASK ) { task_t ct = current_task(); task = (task_t)port->ip_kobject; assert(task != TASK_NULL); if (task_conversion_eval(ct, task)) { ip_unlock(port); return TASK_NULL; } ... return (task); }
其中
task_conversion_eval
就进行了校验,每个task
只能使用他们自己的task ports
,只有kernel task
才有所有的权限:kern_return_t task_conversion_eval(task_t caller, task_t victim) { /* * Tasks are allowed to resolve their own task ports, and the kernel is * allowed to resolve anyone's task port. */ if (caller == kernel_task) { return KERN_SUCCESS; } if (caller == victim) { return KERN_SUCCESS; } /* * Only the kernel can can resolve the kernel's task port. We've established * by this point that the caller is not kernel_task. */ if (victim == kernel_task) { return KERN_INVALID_SECURITY; } #if CONFIG_EMBEDDED /* * On embedded platforms, only a platform binary can resolve the task port * of another platform binary. */ if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) { #if SECURE_KERNEL return KERN_INVALID_SECURITY; #else if (cs_relax_platform_task_ports) { return KERN_SUCCESS; } else { return KERN_INVALID_SECURITY; } #endif /* SECURE_KERNEL */ } #endif /* CONFIG_EMBEDDED */ return KERN_SUCCESS; }
这就意味着哪怕我们拿到了
druid
的task port
,也没有办法通过mach_vm_*
去修改它的任何东西/* * Returns the set of threads belonging to the target task. */ routine task_threads( target_task : task_inspect_t; out act_list : thread_act_array_t);
但是
Bradon
在看一个MIG文件的时候发现有一个函数task_threads
,枚举task
内的线程,重点是这里的参数是task_inspect_t
而非task_t
,这就意味着MIG转换的时候用的并不是convert_port_to_task
而是convert_port_to_task_inspect
,从这个函数的逆向代码中可以看到其中并没有进行task_conversion_eval
,这意味着函数可以执行成功,更有意思的一点是返回的并不是thread_inspect_t rights
,而是thread_act_t
。也就是说,通过task_threads
这个函数,我们将不可修改的task right
替换成了可以修改的thread right
,在线程层次上也不存在说类似task
层面上的校验,也就是说我们可以通过Mach thread API
去绕过task_conversion_eval
。Brandon还在已有的
Mach thread API
上封装了一个能力更强的库threadexec,在Poc中用的就是这个库。步骤3.创建一个新的host层级的异常处理
- 通过
host_get_exception_ports
拿到host level
对于EXC_BAD_ACCESS
的异常处理端口- 分配一个端口作为新的异常处理
- 将
host-priv port
和send right
给我们刚创建的端口- 利用我们在
druid
中的上下文调用host_set_exception_ports
设置我们新的异常处理端口完成之后,任意访问非法内存并且没有注册的异常处理的进程,我们就可以通过
EXC_BAD_ACCESS
异常消息拿到那些进程的task port
,由于这个异常是可恢复的,那么就意味着可以通过task port
去执行代码了。步骤4.拿到ReportCrash的task port
我们之所以要再次获取这个,是因为之前的
ReportCrash
进程已经crash
了
让ReportCrash触发
EXC_BAD_ACCESS
mach_port_t reportcrash = context->reportcrash_service; reportcrash_keepalive_assertion_t reportcrash_assertion = reportcrash_keepalive(reportcrash); if (reportcrash_assertion == 0) { ERROR("Could not generate keepalive assertion for %s", REPORTCRASH_NAME); return false; } ... // 触发EXC_BAD_ACCESS的异常 reportcrash_keepalive_assertion_release(reportcrash_assertion);
因为
ReportCrash
并没有这种消息的处理者,EXC_CRASH
消息的处理者是SafetyNet
,所以异常消息会发送到我们分配的那个端口上去接收到异常消息之后,将
task port
和thread port
保存下来,并让进程恢复利用端口在进程内做代码执行,就像
druid
中一样步骤5.恢复原来的host-level异常处理
接下来的两步并不是一定要做但是最好还是做一下,这样exploit执行完成之后我们并不需要去重启设备或者做别的操作,和之前的系统基本一致。
当我们拿到
ReportCrash
内的代码执行之后,我们应该将原来的host level exception handler
恢复回去,通过druid
调用host_set_exception_ports
去重置异常处理端口:bool ok = threadexec_host_set_exception_ports( context->druid_tx, context->host_priv, EXC_MASK_BAD_ACCESS, context->host_exception_handler, context->host_exception_behavior, context->host_exception_flavor);
步骤6.修复launchd
- 通过
task_for_pid
拿到launchd
的task port
对于我们伪造的每个服务,都进行以下操作:
- 拿到fake service的
port name
- 将fake port和服务都销毁掉
- 调用
mach_port_insert_right
把真实的服务再塞回去0x03.提权过程
都到这里了你还想要什么?
0x04.参考链接
Posts: 2
Participants: 2