CVE-2017-7047 Triple_Fetch 漏洞与利用技术分析

作者:Liang Chen (@chenliang0817)

昨天Google Project Zero的Ian Beer发布了CVE-2017-7047的漏洞细节,以及一个叫Triple_Fetch的漏洞利用app,可以拿到所有10.3.2及以下版本的用户态Root+无沙盒权限,昨天我看了一下这个漏洞和利用的细节,总得来说整个利用思路还是非常精妙的。我决定写这篇文章,旨在尽可能地记录下Triple_Fetch以及CVE-2017-7047的每一个精彩的细节。

CVE-2017-7047漏洞成因与细节

这是个libxpc底层实现的漏洞。我们知道,其实libxpc是在macOS/iOS的mach_msg基础上做了一层封装,使得以前一些因为使用或开发MIG接口的过程中因为对MIG接口细节不熟悉导致的漏洞变得越来越少。有关MIG相关的内容可以参考我以前的文章http://keenlab.tencent.com/en/2016/07/22/WindowServer-The-privilege-chameleon-on-macOS-Part-1/ ,这里不再详细叙述。
XPC自己实现了一套类似于CFObject/OSObject形式的对象库,对应的数据结构为OS_xpc_xxx(例如OS_xpc_dictionary, OS_xpc_data等),当客户端通过XPC发送数据时,_xpc_serializer_pack函数会被调用,将要发送的OS_xpc_xxx对象序列化成binary形式。注意到,如果发送的数据中存在OS_xpc_data对象(可以是作为OS_xpc_array或者OS_xpc_dictionary等容器类的元素)时,对应的serialize函数_xpc_data_serialize会进行判断:

当OS_xpc_data对象的数据大于0x4000时,_xpc_data_serialize函数会调用dispatch_data_make_memory_entry,dispatch_data_make_memory_entry调用mach_make_memory_entry_64。mach_make_memory_entry_64返回给用户一个mem_entry_name_port类型的send right, 用户可以紧接着调用mach_vm_map将这个send right对应的memory映射到自己进程的地址空间。也就是说,对大于0x4000的OS_xpc_data数据,XPC在传输的时候会避免整块内存的传输,而是通过传port的方式让接收端拿到这个memory的send right,接收端接着通过mach_vm_map的方式映射这块内存。接收端反序列化OS_xpc_data的相关代码如下:

之后就是最关键的_xpc_vm_map_memory_entry逻辑了,可以看到,在macOS 10.12.5或者iOS 10.3.2的实现中,调用mach_vm_map的相关参数如下:

mach_vm_map的官方参数定义如下:

值得注意的是最后第四个参数boolean_t copy, 如果是0代表映射的内存与原始进程的内存共享一个物理页,如果是1则是分配新的物理页。
在_xpc_data_deserialize的处理逻辑中,内存通过共享物理页的方式(copy = 0)来映射,这样在客户端进程中攻击者可以随意修改data的内容从而实时体现到接收端进程中。虽然在绝大多数情况下,这样的修改不会造成严重影响,因为接收端本身就应该假设从客户端传入的data是任意可控的。但是如果这片数据中存在复杂结构(例如length等field),那么在处理这片数据时就可能产生double fetch等条件竞争问题。而Ian Beer正是找到了一处”处理这个data时想当然认为这块内存是固定不变的错误”,巧妙地实现了任意代码执行,这部分后面详细叙述,我们先来看看漏洞的修复。

CVE-2017-7047漏洞修复

这个漏洞的修复比较直观,在_xpc_vm_map_memory_entry函数中多加了个参数,指定vm_map是以共享物理页还是拷贝物理页的方式来映射:

可以看到,这里把映射方式改成拷贝物理页后,问题得以解决。

Triple_Fetch利用详解

如果看到这里你还不觉得累,那么下面的内容可能就是本文最精彩的内容了(当然,估计会累)。

一些基本知识

我们现在已经知道,这是个XPC底层实现的漏洞,但具体能否利用,要看特定XPC服务的具体实现,而绝大多数XPC服务仅仅将涉及OS_xpc_data对象的buffer作为普通数据内容来处理,即使在处理的时候buffer内容发生变化,也不会造成大问题。而即便找到有问题的案例,也仅仅是影响部分XPC服务。把一个通用型机制漏洞变成一个只影响部分XPC服务的漏洞利用,可能不是一种好策略。
因此,Ian Beer找到了一个通用利用点,那就是NSXPC。NSXPC是比XPC更上层的一种进程间通信的实现,主要为Objective-c提供进程间通信的接口,它的底层基于XPC框架。我们先来看看Ian Beer提供的漏洞poc:

代码调用了”com.apple.wifi.sharekit”服务的cancelPendingRequestWithToken接口,其第一个参数为一个长度为0x10000,内容全是A的string,我们通过调试的方法来理一下调用这个NSXPC接口最终到底层mach_msg的message结构,首先断点到mach_msg:

观察它的message header结构:

这里发送的是一个复杂消息,长度为0x64。值得注意的是,所有XPC的msgh_id都是固定的0x10000000,这与MIG接口的根据msgh_id号来作dispatch有所不同。由于这个消息用到了大于0x4000的OS_xpc_data数据,因此message_header后跟一个mach_msg_body_t结构,这里的值为1(偏移0x18的4字节),意味着之后跟了一个复杂消息,而偏移0x1c至0x28的内容是一个mach_msg_port_descriptor_t结构,其定义如下:

偏移0x1c处的0x1a03是一个mem_entry_name_port,也就是0x10000的’A’ buffer对应的port。
从0x28开始的8字节为真正的xpc消息的头部,最新的mac/iOS上,这个头信息是固定的: 0x0000000558504321,也就是字符串“!CPX”(XPC!的倒序),以及版本号0x5,接下来跟的是一个序列化过的OS_xpc_dictionary结构:

如果翻译成Human Readable的格式,应该是这样:

这里可以看到,这个serialize后的OS_xpc_data并没有引用对应的send right信息,只是标记它是个DATA(0x8000),以及它的长度0x34000。而事实上,在deserialize的时候,程序会自动寻找mach_msg_body_t中指定的复杂消息个数,并且顺序去寻找后边紧跟的mach_msg_port_descriptor_t结构,而序列化过后的XPC消息中出现的OS_xpc_data与之前填入的mach_msg_port_descriptor_t顺序是一致并且一一对应的。用一个简单明了的图来说明,就是这样:

NSXPC_at_mach_msg_view.png

NSXPC at mach_msg view

看到这里,我们对NSXPC所对应的底层mach_msg结构已经有所了解。但是,这里还遗留了个问题:如果所有XPC的msgh_id都是0x10000000,那么接收端如何知道我调用的是哪个接口呢?其中的奥秘,就在这个XPC Dictionary中的root字段,我们还没有看过这个字段对应的mem_entry_name_port对应的buffer内容是啥呢,找到这个buffer后,他大概就是这个样子:

这是个bplist16序列化格式的buffer,是NSXPC专用的,和底层XPC的序列化格式是有区别的。这个buffer被做成mem_entry_name_port传输给接收端,而接收端直接用共享内存的方式获得这个buffer,并进行反序列化操作,这就创造了一个绝佳的利用点,当然这是后话。我们先看一下这个buffer的二进制内容:

bplist32sample1.png

bplist sample to call cancelPendingRequestWithToken

这个bplist16格式的解析比较复杂,而且Ian Beer的实现里也只是覆盖了部分格式,大致转换成Human Readable的形式就是这样:

这里的ty字段是这个objc接口的函数原型,se是selector名称,也就是接口名字,后面跟的AAAA就是他的参数内容。接收端的NSXPC接口正是根据这个bplist16中的内容来分发到正确的接口并给予正确的接口参数的。
Ian Beer提供的PoC是跑在macOS下的,因此他直接调用了NSXPC的接口,然后通过DYLD_INSERT_LIBRARIES注入的方式hook了mach_make_memory_entry_64函数,这样就能获取这个send right并且进行vm_map。但是在iOS上(特别是没有越狱的iOS)并不能做这样的hook,如果从NSXPC接口入手我们没有办法获得那块共享内存(其实是有办法的:),但不是很优雅),所以Ian Beer在Triple_Fetch利用程序中自己实现了一套XPC与NSXPC对象封装、序列化、反序列化的库,自己组包并调用mach_msg与NSXPC的服务端通信,实现了利用。

Triple_Fetch利用 - 如何实现控PC

Ian Beer对NSXPC的这个bplist16的dictionary中的ty字段做了文章,这个字段指定了objc接口的函数原型,NSXPC底层会去解析这个string,如果@后跟了个带引号的字符串,例如:@”mfz”,则CoreFoundation中的__NSMS函数会被调用:

这个函数的第一个参数指向bplist16共享内存偏移到ty字段@开始的地方,该函数负责解析后面的字串,关键逻辑如下:

Ian Beer构造的初始字符串是@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00, 其中mfz字串是运行时随机生成的3个随机字母,这是为了避免Foundation对已经出现过的字符串进行cache而不分配新内存(因为利用需要多次触发尝试)。

  1. 在A处,调用__NSGetSizeAndAlignment得到的长度是6(因为@”mfz”长度为6),因此calloc分配的内存长度是48(42 + 6)。而buffer的前37字节用于存储metadata,所以真正的字符串会拷贝在buffer+37的地方。

  2. 在计算并分配好“合理“长度的buffer后,__NSMS1函数在B处重新扫描这个字符串,找到第二个引号的位置(正常情况下,也就是@”mfz”的第二个引号位置),但需要注意,在第二个引号出现之前,不能有null string

  3. 在C处,程序根据刚才计算的”第二个引号”的位置,开始拷贝字串到buffer+37位置。

Ian Beer通过在客户端app操作共享内存,改变@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00的某几字节,构造出一个绝妙的Triple_Fetch的状态,使得:

  1. 在A处计算长度时,字符串是@”mfz”AAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00,因此calloc了48字节(6+42)

  2. 在B处,字符串变为@”mfzAAAAAA\x20\x40\x20\x20\x01\x41\x41\x41”\x00, 这样第二个引号到了倒数第二个字节的位置(v57的位置)

  3. 在C处,字符串变为@”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00”\x00,程序将整个@”mfzAAAAAA\x20\x40\x20\x20\x01\x00\x00\x00”拷贝到buffer+37位置

如果只是要触发堆溢出,那1和2构造的double fetch已经足够,但如果要控PC,Ian Beer选择的是覆盖buffer后面精心分布的OS_xpc_uuid的对象,该对象大小恰巧也是48字节,并且其前8字节为obj-c的isa(类似c++的vptr指针),并且其某些字段是可控的(uuid string部分),通过覆盖这个指针,使其指向一段spray过的gadget buffer进行ROP,完成任意代码执行。但由于iOS下heap分配的地址高4位是1,所以\x20\x40\x20\x20\x01\x41\x41\x41不可能是个有效的heap地址,因此我们必须加上状态3,用triple fetch的方式实现代码执行。
下图展示了溢出时的内存分布:

overflow.png

overflow to OS_xpc_uuid

在NSXPC消息处理完毕后,这些布局的OS_xpc_uuid就会被释放,因为其isa指针已被覆盖,并且新的指针0x120204020指向了可控数据,在执行xpc_release(uuid)的时候就能成功控制PC。

布局与堆喷射

布局有两个因素需要考虑,其一是需要在特定内存0x120204020地址上填入rop gadget,其二是需要在0x30大小的block上喷一些OS_xpc_uuid对象,这样当触发漏洞calloc(1,48)的时候,让分配的对象后面紧跟一个OS_xpc_uuid对象。
第一点Ian Beer是通过在发送的XPC message里加入了200个“heap_sprayXXX”的key,他们的value各自对应一个OS_xpc_data,指向0x4000 * 0x200的大内存所对应的send right,这块大内存就是ROP gadget。
而第二点是通过在XPC message里加入0x1000个OS_xpc_uuid,为了创造一些hole放入freelist中,使得我们的calloc(1,48)能够占入, Ian Beer在add_heap_groom_to_dictionary函数中采用了一些技巧,比如间隔插入一些大对象等,但我个人觉得这里的groom并不是很有必要,因为我们不追求一次触发就利用成功(事实也是如此),每次触发失败后当OS_xpc_uuid释放后,就会天然地产生很多0x30 block上的free element,下一次触发漏洞时就比较容易满足理想的堆分布状态。

ROP与代码执行

当接收端处理完消息后xpc_release(uuid)就会被触发,而我们把其中一个uuid对象的isa替换后,我们就控制了pc。 此事我们的x0寄存器指向OS_xpc_uuid对象,而这个对象的0x18-0x28的16字节是可控的。 Ian Beer选择了这么一段作为stack_pivot的前置工作:

这样就完美地将x0指向了我们完全可控的buffer了。

ROP如何获取目标进程的send right

由于ROP执行代码比较不优雅,效率也低,Ian Beer在客户端发送mach_msg时,在XPC message的dictionary中额外加入了0x1000个port,将其spray到接收端进程,由于port_name的值在分配的时候是有规律的,接收端在ROP的时候调用64次mach_msg,remote_port设置成从0xb0003开始,每次+4,而reply_port设置为自己进程的task port,消息id设置为0x12344321。在这64次发送中,只要有一次send right port_name猜中,客户端就可以拿着port_set中的receive right尝试接收消息,如果收到的消息id是0x12344321那客户端拿到的remote port就是接收端进程的task send right。

接收端进程的选择

由于是通杀NSXPC的利用,只要是进程实现了NSXPC的服务,并且container沙盒允许调用,我们都可以实现对端进程的代码执行。尽管如此,接收端进程的选择还是至关重要的。简单的来讲,我们首选的服务进程当然是Root权限+无沙盒,并且服务以OnDemand的形式来启动。这样的服务即使我们攻击失败导致进程崩溃,用户也不会有任何感觉,而且可以重复尝试攻击直到成功。
Ian Beer在这里选择了coreauthd进程,还有一个重要的原因,是它可以通过调用processor_set_tasks来获取系统任意进程的send right从而绕过进程必须有get-task-allow entitlement才能获取其他进程send right的限制。而这个技巧Jonathan Levin在2015年已经详细阐述,可以参考:http://newosxbook.com/articles/PST2.html 。

后期利用

在拿到coreauthd的send right后,Ian Beer调用thread_create_running在coreauthd中起一个线程,调用processor_set_tasks来获得系统所有进程的send right。然后拿着amfid的send right用与mach portal同样的姿势干掉了代码签名,最后运行debugserver实现调试任意进程。

原文:http://keenlab.tencent.com/zh/2017/08/02/CVE-2017-7047-Triple-Fetch-bug-and-vulnerability-analysis/



发表评论

(必填)

(必填)

(以便回访)