作者:turingH
為學大病在好名。
0x00 摘要
去年在分析CVE-2016-1757時,初步的接觸了Mach在IPC系統中使用的Message,在分析最近的一系列與IPC模塊相關的漏洞時,又加強對IPC模塊的理解,所以通過一到兩篇文章梳理一下最近的學習總結與心得體會。
關于CVE-2016-7637這個漏洞的描述有很多資料了,是一個攻擊Mach的內核IPC模塊的漏洞,本文最后會對漏洞做出比較詳細的解釋,這里給出幾個鏈接,不熟悉這個漏洞的讀者可以先了解一下。
Broken kernel mach port name uref
0x01 什么是Port
對于一般的開發者,在用到Port的時候可以簡單的將Port理解為進程間通信所使用的類似于Socket的東西,可以用他來發送消息,也可以用來接收消息。
下面我們來一步一步的構建對整個模塊的理解。
已經知道Port是什么的讀者可以跳過這一個部分。
1.1 利用Port傳遞數據
Port最簡單的可以理解為一個內核中的消息隊列。不同的Task通過這個消息隊列相互傳遞數據。而Port就是用于找到這個隊列的索引。

1.2 Port、Port Name 與 Right
通過函數mach_port_allocate就可以在內核中建立一個消息隊列,并獲取一個與之對應的的Port。代碼如下。
mach_port_t p;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
通過查看內核源碼,
/*
* Purpose:
* Allocates a right in a space. Like mach_port_allocate_name,
* except that the implementation picks a name for the right.
* The name may be any legal name in the space that doesn't
* currently denote a right.
*/
kern_return_t
mach_port_allocate(
ipc_space_t space,
mach_port_right_t right,
mach_port_name_t *namep)
{
kern_return_t kr;
mach_port_qos_t qos = qos_template;
kr = mach_port_allocate_full (space, right, MACH_PORT_NULL,
&qos, namep);
return (kr);
}
仔細看的話會發現,對我們剛剛申請的p出現了好幾個解釋,一下就暈了。
- 在應用層代碼中,p被定義為
mach_port_t。 - 在內核中代碼中,
namep是mach_port_name_t。 - 在注釋中又說,Allocates a RIGHT in a space。
1.2.1 Port與Port Name
通過調試器觀察一下,可以發現,
(lldb) p p
(mach_port_t) $0 = 3331
* frame #0: 0xffffff801d4ee11d kernel`mach_port_allocate_full(space=0xffffff8024ceac00, right=1, proto=0x0000000000000000, qosp=0xffffff887d7b3ef0, namep=0xffffff887d7b3eec
在應用層的mach_port_t是一個已經經過代碼處理的類似于Socket的一個整數,來表示這個Port。
而namep在內核之中是一個地址,指向了一塊用來索引Port的內存,具體的實現在本文的后面會有更詳細的解釋。
1.2.2 Right
這里注釋所說的Right簡單的理解,其實是一個Port和對這個Port進行訪問的權限。每一個Port代表的消息隊列并不是可以任意訪問的,需要有對這個隊列的訪問權限。各種權限在頭文件中的定義如下。
#define MACH_PORT_RIGHT_SEND ((mach_port_right_t) 0)
#define MACH_PORT_RIGHT_RECEIVE ((mach_port_right_t) 1)
#define MACH_PORT_RIGHT_SEND_ONCE ((mach_port_right_t) 2)
#define MACH_PORT_RIGHT_PORT_SET ((mach_port_right_t) 3)
#define MACH_PORT_RIGHT_DEAD_NAME ((mach_port_right_t) 4)
#define MACH_PORT_RIGHT_NUMBER ((mach_port_right_t) 5)
每種Right都有不同的含義,可以自行查閱文檔。
這里需要簡單的提一下的就是每一個Port都有且只有Task對其擁有RECEIVE的權限,SEND的權限不限。擁有MACH_PORT_RIGHT_RECEIVE時也可以對Port進行消息的Send。

1.3 Port的具體實現
閱讀mach_port_allocate_full函數的源碼,最終在一個port的創建流程中,最主要的函數是ipc_port_alloc以及在其實現中調用的ipc_object_alloc。
kern_return_t
ipc_port_alloc(
ipc_space_t space,
mach_port_name_t *namep,
ipc_port_t *portp)
{
ipc_port_t port;
mach_port_name_t name;
kern_return_t kr;
kr = ipc_object_alloc(space, IOT_PORT,
MACH_PORT_TYPE_RECEIVE, 0,
&name, (ipc_object_t *) &port);
if (kr != KERN_SUCCESS)
return kr;
/* port and space are locked */
ipc_port_init(port, space, name);
[...]
*namep = name; <--namep是從這里來的。
*portp = port;
return KERN_SUCCESS;
}
很明顯,port和name兩個變量都是由函數ipc_object_alloc中獲取的。關鍵源碼如下。
kern_return_t
ipc_object_alloc(
ipc_space_t space,
ipc_object_type_t otype,
mach_port_type_t type,
mach_port_urefs_t urefs,
mach_port_name_t *namep,
ipc_object_t *objectp)
{
ipc_object_t object;
ipc_entry_t entry;
kern_return_t kr;
[...]
//從zone中申請內存,這個object就是ipc_port
object = io_alloc(otype);
[...]
//獲取namep和entry
*namep = CAST_MACH_PORT_TO_NAME(object);
kr = ipc_entry_alloc(space, namep, &entry);
if (kr != KERN_SUCCESS) {
io_free(otype, object);
return kr;
}
/* space is write-locked */
//設置關鍵的參數
entry->ie_bits |= type | urefs;
entry->ie_object = object;
ipc_entry_modified(space, *namep, entry);
io_lock(object);
object->io_references = 1; /* for entry, not caller */
object->io_bits = io_makebits(TRUE, otype, 0);
*objectp = object;
return KERN_SUCCESS;
}
1.3.1 IPC Space 和 IPC Entry
細心的讀者會發現前面有個叫做space的參數沒有解釋。這里又出現了一個新的結構叫做entry。他們是有關系的,這里我們來一起解釋一下。
Each task has a private IPC spacea namespace for portsthat is represented by the ipc_space structure in the kernel.
Mac OS X Internals
每一個Task都有一個自己獨立的IPC的數據空間,就是這里的space。他的數據結構是定義如下。
// osfmk/ipc/ipc_space.h
typedef natural_t ipc_space_refs_t;
struct ipc_space {
decl_mutex_data(,is_ref_lock_data)
ipc_space_refs_t is_references;
decl_mutex_data(,is_lock_data)
// is the space active?
boolean_t is_active;
// is the space growing?
boolean_t is_growing;
// table (array) of IPC entries
// 這個是最重要的,存放了所有的entry
ipc_entry_t is_table;
// current table size
ipc_entry_num_t is_table_size;
// information for larger table
struct ipc_table_size *is_table_next;
// splay tree of IPC entries (can be NULL)
struct ipc_splay_tree is_tree;
// number of entries in the tree
ipc_entry_num_t is_tree_total;
// number of "small" entries in the tree
ipc_entry_num_t is_tree_small;
// number of hashed entries in the tree
ipc_entry_num_t is_tree_hash;
// for is_fast_space()
boolean_t is_fast;
};
而我們研究的對象 Mach Ports全都存儲在is_table這個數組中,這個數組就是由ipc_entry組成的。
struct ipc_entry {
struct ipc_object *ie_object; //ipc_port_t
ipc_entry_bits_t ie_bits; //gen|0|0|0|capability|user reference
mach_port_index_t ie_index;
union {
mach_port_index_t next; /* next in freelist, or... */
ipc_table_index_t request; /* dead name request notify */
} index;
};
用一張書上的圖,很清楚的表明了他們之間的關系。

1.3.2 ipc_port
相對而言ipc_port的數據結構就較為簡單了。在復制給space的ie_object之后,通過ipc_port_init函數的初始化,就完成了port的創建了。
ipc_port_init(
ipc_port_t port,
ipc_space_t space,
mach_port_name_t name)
{
/* port->ip_kobject doesn't have to be initialized */
port->ip_receiver = space;
port->ip_receiver_name = name;
port->ip_mscount = 0;
port->ip_srights = 0;
port->ip_sorights = 0;
port->ip_nsrequest = IP_NULL;
port->ip_pdrequest = IP_NULL;
port->ip_requests = IPR_NULL;
port->ip_premsg = IKM_NULL;
port->ip_context = 0;
port->ip_sprequests = 0;
port->ip_spimportant = 0;
port->ip_impdonation = 0;
port->ip_tempowner = 0;
port->ip_guarded = 0;
port->ip_strict_guard = 0;
port->ip_impcount = 0;
port->ip_reserved = 0;
ipc_mqueue_init(&port->ip_messages,
FALSE /* !set */, NULL /* no reserved link */);
}
這里同樣的用書上的一張圖就可以很簡單的解釋清楚了。

0x02 POC的分析
? POC原來的writeup在這里。
? 原文已經解釋的非常清楚了,我就不畫蛇添足了。簡單記錄一下我自己在分析的過程中的一些問題。
2.1 port的user reference計數代表了什么?
? 一個port的user reference只表示了某個entry在task的space中被多少個地方使用,和entry實際指向哪個port沒有關系。
?
2.2 ipc_right_dealloc函數是只釋放了entry還是同時也在內存中釋放了port?
? ipc_right_dealloc函數相關部分源碼如下:
kern_return_t
ipc_right_dealloc(
ipc_space_t space,
mach_port_name_t name,
ipc_entry_t entry)
{
ipc_port_t port = IP_NULL;
ipc_entry_bits_t bits;
mach_port_type_t type;
bits = entry->ie_bits;
type = IE_BITS_TYPE(bits);
assert(is_active(space));
switch (type) {
[...]
case MACH_PORT_TYPE_SEND: {
[...]
port = (ipc_port_t) entry->ie_object;
[...]
//如果在task內entry的reference已經為1了就
//釋放entry
//如果計數不為1,就將計數減一
if (IE_BITS_UREFS(bits) == 1) {
if (--port->ip_srights == 0) {
nsrequest = port->ip_nsrequest;
if (nsrequest != IP_NULL) {
port->ip_nsrequest = IP_NULL;
mscount = port->ip_mscount;
}
}
[...]
entry->ie_object = IO_NULL;
ipc_entry_dealloc(space, name, entry);
is_write_unlock(space);
ip_release(port);
} else {
ip_unlock(port);
entry->ie_bits = bits-1; /* decrement urefs */
ipc_entry_modified(space, name, entry);
is_write_unlock(space);
}
[...]
return KERN_SUCCESS;
}
所以當entry計數為1的時候,調用了ipc_entry_dealloc,ipc_entry_dealloc不會將entry對應的內存釋放,而是將其放入一個free_list等待重復使用。entry的內存不會釋放,而且entry在is_table中的index也并不會改變,只是被放到了一個結構管理的隊列中去了。
對于Port來說,內核調用了ip_release,這個函數的作用是減少ipc_object自身的reference,如果port的索引變為0了,那就會被釋放,如果系統中還有其他的進程在使用這個port,那么這個port就不會被釋放。
2.3 如何通過調試器調試漏洞觸發的現場?
一開始我想的方法是對ipc_right_copyout下條件斷點,條件是entry->ie_bits&0xffff == 0xfffe。但是因為ipc_right_copyout這個函數在內核中的調用太過于頻繁,導致虛擬機跑太卡了。
只能通過逆向,在匯編代碼處下斷點。(內核版本10.12_16A323)

對應的就是出bug的代碼段。
if (urefs+1 == MACH_PORT_UREFS_MAX) {
if (overflow) {
/* leave urefs pegged to maximum */ <---- (1)
port->ip_srights--;
ip_unlock(port);
ip_release(port);
return KERN_SUCCESS;
}
ip_unlock(port);
return KERN_UREFS_OVERFLOW;
}
所以通過斷點
b *(0xffffff80002e6fbb + kslide)
就可以得到漏洞觸發時的情況。

2.4 port替換的原理是什么?
port是通過port name在task中來獲取的。在前文中提到,namep是通過函數ipc_entry_alloc來獲取的。查看獲取到namep的核心代碼如下:
kern_return_t
ipc_entry_claim(
ipc_space_t space,
mach_port_name_t *namep,
ipc_entry_t *entryp)
{
ipc_entry_t entry;
ipc_entry_t table;
mach_port_index_t first_free;
mach_port_gen_t gen;
mach_port_name_t new_name;
table = &space->is_table[0];
first_free = table->ie_next;
assert(first_free != 0);
entry = &table[first_free]; //[1]
table->ie_next = entry->ie_next;
space->is_table_free--;
assert(table->ie_next < space->is_table_size);
/*
* Initialize the new entry. We need only
* increment the generation number and clear ie_request.
*/
gen = IE_BITS_NEW_GEN(entry->ie_bits); //[2]
entry->ie_bits = gen;
entry->ie_request = IE_REQ_NONE;
/*
* The new name can't be MACH_PORT_NULL because index
* is non-zero. It can't be MACH_PORT_DEAD because
* the table isn't allowed to grow big enough.
* (See comment in ipc/ipc_table.h.)
*/
new_name = MACH_PORT_MAKE(first_free, gen); //[3]
assert(MACH_PORT_VALID(new_name));
*namep = new_name;
*entryp = entry;
return KERN_SUCCESS;
}
通過[1]可以看到,正如2.2節提到的一樣,entry在table中的index是不變的。
通過[2]可以看到,每次使用entry來存放一個新port時,gen的值會加1。
通過[3]可以看到,namep就是通過這兩個參數生成的。
因為index是不變的,所以在通過漏洞釋放target_port之后,不斷的對目標entry進行申請和釋放,就可以通過整形溢出,使得gen變成和taget_port釋放之前使用的entry相同。
那么就實現了在port_name不變的情況下替換了port的內核對象。
0x03 小結
通過CVE-2016-7637的分析和研究加深了對port這個數據結構的理解,并且通過對poc的分析,體現了一個port在單個task中的狀態變化,實際上是ipc_space和ipc_entry狀態變化。
接下來就要分析學習CVE-2016-7644,通過對CVE-2016-7644的分析學習,可以更加深入的理解port在內核中狀態的變化。也就是port自身的port->srights和io_reference的狀態變化及漏洞的利用。
參考
-
《Mac OS X Internals》
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/197/
暫無評論