深入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
物件的另一個例子如下:
在 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物件結構可能有所不同。
四、參考資料
ofollow,noindex" target="_blank">https://thecyberwire.com/events/docs/IanBeer_JSS_Slides.pdf
OS Internals, Volume I: User Mode by Jonathan Levin