一、前言
最近我在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
对象的另一个例子如下:
在LLDB
中xpc_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_mask
及objc_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