深入XPC:逆向分析XPC对象

 

一、前言

最近我在FortiGuard实验室一直在深入研究macOS系统安全,主要关注的是发现和分析IPC漏洞方面内容。在本文中,我将与大家分享XPC内部数据类型,可以帮助研究人员(包括我自己)快速分析XPC漏洞根源,也能深入分析针对这些漏洞的利用技术。

XPC是macOS/iOS系统上使用的增强型IPC框架,自10.7/5.0版引入以来,XPC的使用范围已经呈爆炸式增长。XPC依然包含没有官方说明文档的大量功能,具体实现也没有公开(例如,libxpc这个主工程为闭源项目)。XPC在两个层面上开放API:底层以及Foundation封装层。在本文中我们只关注底层API,这些API为libxpc.dylib直接导出的xpc_*函数。

这些API可以分为object API以及transport API。XPC通过libxpc.dylib提供自己的数据类型,具体数据类型如下所示:

图1. XPC提供的数据类型

从C API角度来看,所有的对象实际上都是xpc_object_t。实际类型可以通过xpc_get_type(xpc_object_t)函数动态确定。所有数据类型可以使用对应的xpc_objectType_create函数创建,并且所有这些函数都会调用_xpc_base_create(Class, Size)函数,其中Size参数指定了对象的大小,而Class参数为某个_OS_xpc_type_*元类(metaclass)。

我们可以通过Hopper Disassembler v4看到 _xpc_base_create函数被多次引用。

图2. 对_xpc_base_create函数的引用代码

我开发了Hopper的一个python脚本,可以自动找出调用_xpc_base_create函数时所使用的具体参数。如下python脚本可以显示Hopper Disassembler中XPC对象的大小。

def get_last2instructions_addr(seg, x):
                  last1ins_addr = seg.getInstructionStart(x - 1)
                  last2ins_addr = seg.getInstructionStart(last1ins_addr - 1)
                  last2ins = seg.getInstructionAtAddress(last2ins_addr)
                  last1ins = seg.getInstructionAtAddress(last1ins_addr)
                  print hex(last2ins_addr), last2ins.getInstructionString(), last2ins.getRawArgument(0), last2ins.getRawArgument(1)
                  print hex(last1ins_addr), last1ins.getInstructionString(), last1ins.getRawArgument(0), last1ins.getRawArgument(1)
                  return last2ins,last1ins
def run():
                  print '[*] Demonstrating XPC ojbect sizes using a hopper diassembler's python script'
                  xpc_object_sizes_dict = dict()
                  doc = Document.getCurrentDocument()
                  _xpc_base_create_addr = doc.getAddressForName('__xpc_base_create')
                  for i in range(doc.getSegmentCount()):
                                    seg = doc.getSegment(i)
                                    #print '[*]'+ seg.getName()
                                    if('__TEXT' == seg.getName()):
                                                      eachxrefs = seg.getReferencesOfAddress(_xpc_base_create_addr)
                                                      for x in eachxrefs:
                                                                        last2ins,last1ins = get_last2instructions_addr(seg,x)
                                                                        p = seg.getProcedureAtAddress(x)
                                                                        p_entry_addr =  p.getEntryPoint()
                                                                        pname = seg.getNameAtAddress(p_entry_addr)
                                                                        x_symbol = pname + '+' + hex(x - p_entry_addr)
                                                                        print hex(x),'(' + x_symbol + ')'
                                                                        ins0 = seg.getInstructionAtAddress(x - 5)
                                                                        ins1 = seg.getInstructionAtAddress(x - 12)
                                                                        if last2ins.getInstructionString() == 'mov' and last1ins.getInstructionString() == 'lea':
                                                                                          if last2ins.getRawArgument(0) == 'esi' and last1ins.getRawArgument(0) == 'rdi':
                                                                                                            indirect_addr = int(last1ins.getRawArgument(1)[7:-1],16)
                                                                                                            xpcObj_len = last2ins.getRawArgument(1)
                                                                                                            callerinfo = '__xpc_base_create('+ doc.getNameAtAddress(indirect_addr)+',' + xpcObj_len+ ');'
                                                                                                            if callerinfo not in xpc_object_sizes_dict.keys():
                                                                                                                              xpc_object_sizes_dict[callerinfo] = '#from ' + x_symbol
                                                                                                            else:
                                                                                                                              xpc_object_sizes_dict[callerinfo] = xpc_object_sizes_dict[callerinfo] + ',' + x_symbol
                                                                                                            print callerinfo
                                                                                                            #xpc_object_sizes_list.append(callerinfo)
                                                                        elif last2ins.getInstructionString() == 'lea' and last1ins.getInstructionString() == 'mov':
                                                                                          if last2ins.getRawArgument(0) == 'rdi' and last1ins.getRawArgument(0) == 'esi':
                                                                                                            indirect_addr = int(last2ins.getRawArgument(1)[7:-1],16)
                                                                                                            xpcObj_len = last1ins.getRawArgument(1)
                                                                                                            callerinfo = '__xpc_base_create('+ doc.getNameAtAddress(indirect_addr)+',' + xpcObj_len+ ');'
                                                                                                            if callerinfo not in xpc_object_sizes_dict.keys():
                                                                                                                              xpc_object_sizes_dict[callerinfo] = '#from ' + x_symbol

                                                                                                            else:
                                                                                                                              xpc_object_sizes_dict[callerinfo] = xpc_object_sizes_dict[callerinfo] + ',' + x_symbol
                                                                                                            print callerinfo
                                                                                                            #xpc_object_sizes_list.append(callerinfo)
                                                                        elif last2ins.getInstructionString() == 'lea' and last1ins.getInstructionString() == 'lea':
                                                                                          if last2ins.getRawArgument(0) == 'rsi' and last1ins.getRawArgument(0) == 'rdi':
                                                                                                            indirect_addr = int(last1ins.getRawArgument(1)[7:-1],16)
                                                                                                            xpcObj_len = last2ins.getRawArgument(1)[7:-1]
                                                                                                            callerinfo = '__xpc_base_create('+ doc.getNameAtAddress(indirect_addr)+',' + xpcObj_len+ ');'
                                                                                                            if callerinfo not in xpc_object_sizes_dict.keys():
                                                                                                                              xpc_object_sizes_dict[callerinfo] = '#from ' + x_symbol
                                                                                                            else:
                                                                                                                              xpc_object_sizes_dict[callerinfo] = xpc_object_sizes_dict[callerinfo] + ',' + x_symbol
                                                                                                            print callerinfo
                                                                                                            #xpc_object_sizes_list.append(callerinfo)
                                                                                          elif last2ins.getRawArgument(0) == 'rdi' and last1ins.getRawArgument(0) == 'rsi':
                                                                                                            indirect_addr = int(last2ins.getRawArgument(1)[7:-1],16)
                                                                                                            xpcObj_len = last1ins.getRawArgument(1)[7:-1]
                                                                                                            callerinfo = '__xpc_base_create('+ doc.getNameAtAddress(indirect_addr)+',' + xpcObj_len+ ');'
                                                                                                            if callerinfo not in xpc_object_sizes_dict.keys():
                                                                                                                              xpc_object_sizes_dict[callerinfo] = '#from ' + x_symbol
                                                                                                            else:
                                                                                                                              xpc_object_sizes_dict[callerinfo] = xpc_object_sizes_dict[callerinfo] + ',' + x_symbol
                                                                                                            print callerinfo
                                                                                                            #xpc_object_sizes_list.append(callerinfo)
                                                                        print '____________________________________________________________'
                  dict_len = len(xpc_object_sizes_dict)
                  print '[*] Total of XPC object: %d' % dict_len
                  for key in xpc_object_sizes_dict.keys():
                                    print key, xpc_object_sizes_dict[key]
if __name__ == '__main__':
                  run()

运行该脚本后,我们可以看到所有XPC对象大小,如下所示:

__xpc_base_create(_OBJC_CLASS_$_OS_xpc_serializer,0x98);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_mach_send,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_activity,0x78);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_data,0x28);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_double,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_file_transfer,0x48);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_service_instance,0x78);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_uint64,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_bundle,0x238);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_pointer,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_string,0x10);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_pipe,r12+0x20);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_connection,r14+0xa8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_shmem,0x18);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_dictionary,0xa8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_uuid,0x10);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_connection,0xa8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_endpoint,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_int64,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_date,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_fd,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_mach_recv,0x10);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_bool,0x8);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_array,0x10);
__xpc_base_create(_OBJC_CLASS_$_OS_xpc_service,0x5d);

图3. python脚本输出结果,显示XPC对象大小

此时我们已经知道所有不同数据类型的XPC对象的大小。接下来我们可以看一下_xpc_base_create函数的实现。

图4. _xpc_base_create函数的实现

可以看到XPC对象的实际大小等于Size参数+0x18

然后我们需要进行一些逆向分析工作,检查所有对象的内存布局。在本文中,我想与大家分享主要类型的分析过程,其他类型会在后续文章中详细介绍。

 

二、主要类型分析

xpc_int64_t

我们可以使用xpc_int64_create函数来创建一个xpc_int64_t对象,如下所示:

使用LLDB观察xpc_int64_t对象的内存布局:

xpc_uint64_t对象的结构如下所示:

图5. xpc_uint64_t结构

xpc_uint64_t

使用xpc_uint64_create函数创建xpc_uint64_t对象,代码如下:

可以看到返回值不是有效的内存地址。我们需要在输入参数上执行一些算数运算来生成返回值。在这个例子中,XPC直接使用64位unsigned integer来表示xpc_uint64_t对象。

创建xpc_uint64_t对象的另一个例子如下:

LLDBxpc_uint64_t对象的内存布局如下所示:

可以看到返回值指向的内存缓冲区对应的是xpc_uint64_t对象,且输入参数位于0x18偏移地址处。

接下来我们可以深入分析xpc_uint64_create函数的具体实现,如下所示:

图6. _xpc_uint64_create函数具体实现

在该函数中,代码首先会将参数逻辑右移52位。

a) 如果结果不等于0,则会调用_xpc_base_create函数来创建XPC对象,然后将0x08(4字节长)写入0x14偏移处的缓冲区。最后,代码将参数(8字节长)写入0x18偏移处的缓冲区。

b) 如果结果等于0且全局变量objc_debug_taggedpointer_mask不等于0,那么就会执行(value << 0xc | 0x4f) ^ objc_debug_taggedpointer_obfuscator。在LLDB调试器中,我们可以看到objc_debug_taggedpointer_obfuscator变量等于0x5de9b03e5c731aae,因此运算结果会等于0x5de9b42a48670ae1,这个值即为_xpc_uint64_create函数的返回值。如果结果为0,那么就与a)情况相同。

我们可以检查全局变量objc_debug_taggedpointer_maskobjc_debug_taggedpointer_obfuscator的值,如下所示:

一旦我们知道objc_debug_taggedpointer_obfuscator的值,我们就可以计算出返回值。

每个新进程实例所对应的objc_debug_taggedpointer_obfuscator都为随机值。现在我们可以跟踪一下这个变量的生成过程。

可以看到,objc_debug_taggedpointer_obfuscator实际上是libobjc.A.dylib库中的一个全局变量。如下代码(源文件:objc4-750/runtime/objc-runtime-new.mm)可以用来生成随机的objc_debug_taggedpointer_obfuscator:

图7. 初始化objc_debug_taggedpointer_obfuscator变量

可以使用void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)函数完成初始化工作,具体参考objc-runtime-new.mm中的源代码。在二进制镜像的初始化阶段中,我们还可以看到随机化的objc_debug_taggedpointer_obfuscator全局变量生成过程。

最后我们再给出xpc_uint64_t对象的结构,如下所示:

图8. xpc_uint64_t对象结构

xpc_uuid_t

我们可以使用xpc_uuid_create函数来创建xpc_uuid_t对象(UUID为universally unique identifier的缩写),如下所示:

LLDB中查看xpc_uuid_t对象的内存布局,如下所示:

根据内存布局信息,我们可以轻松澄清xpc_uuid_t对象的结构:

图9. xpc_uuid_t对象结构

xpc_double_t

我们可以使用xpc_double_create函数来创建xpc_double_t对象,如下所示:

LLDB中查看xpc_double_t对象的内存布局:

xpc_double_t对象的结构如下所示:

图10. xpc_double_t对象结构

xpc_date_t

我们可以使用xpc_date_create函数来创建xpc_date_t对象,如下所示:

LLDB中查看xpc_date_t对象的内存结构:

xpc_date_t对象的结构如下所示:

图11. xpc_date_t对象结构

xpc_string_t

可以使用xpc_string_create函数创建xpc_string_t对象,如下所示:

LLDB中查看xpc_string_t对象的内存布局:

xpc_string_t对象的结构如下所示:

图12. xpc_string_t对象结构

xpc_array_t

可以使用xpc_array_create函数创建xpc_array_t对象,如下所示:

在这个例子中,我们首先创建了一个xpc_array_t对象,然后将3个值加入数组中。xpc_array_create函数声明如下:

xpc_array_create函数的实现如下所示:

图13. xpc_array_create函数实现代码

从上图中,我们可知数组的大小等于(count*2+0x08),这个值存放在0x1c偏移处(4字节大小)。指向已分配缓冲区的指针存放于0x20偏移处,已分配缓冲区的大小等于(count*2+0x8)*0x8

LLDB中观察该对象的内存布局,如下所示:

数组的长度存放于0x18偏移处(4字节)。0x20偏移处的指针指向的是已分配的xpc_object_t缓冲区,缓冲区中存放的是数组中的所有元素(xpc_object_t)。xpc_array_t对象的结构如下所示:

图14. xpc_array_t对象结构

xpc_data_t

可以使用xpc_data_create函数创建xpc_data_t对象,如下所示:

LLDB中观察xpc_data_t对象的内存布局:

xpc_data_t对象的结构如下图所示:

图15. xpc_data_t对象结构

如果数据缓冲区的长度大于等于0x4000,那么0x14偏移处的值则会等于(length+0x7)&0xfffffffc,否则就等于0x04

xpc_dictionary_t

xpc_dictionary_t类型在XPC中扮演着重要角色。端点间所有消息都以字典格式传递,这样序列化/反序列化处理起来更加方便。与其他主要类型相比,xpc_dictionary_t的内部构造更为复杂。让我们一步一步揭开面纱。

可以使用xpc_dictionary_create函数创建xpc_dictionary_t对象,如下所示。

LLDB中观察xpc_dictionary_t对象的内存布局。

hash_buckets字段是长度为7的一个数组,hash_buckets[7]中的每个元素存放的是XPC字典链表项。比如,hash_buckets[3]的内存布局如下所示:

可以确定XPC字典链表项的结构如下所示:

图16. XPC字典链表项结构

最后,我们再给出xpc_dictionary_t对象的结构,如下所示。

目前我们已经讨论了XPC对象的主要数据类型,也分析了这些对象的内部结构及内存布局。了解内部结构后,我们不仅能快速分析XPC中的漏洞,也能在跟踪和解析XPC相关漏洞利用技术中事半功倍。

 

三、调试环境

macOS Mojave version 10.14.1

需要注意的是,其他macOS版本上这些XPC对象结构可能有所不同。

 

四、参考资料

https://thecyberwire.com/events/docs/IanBeer_JSS_Slides.pdf

OS Internals, Volume I: User Mode by Jonathan Levin

(完)