來源:turingh.github.io

作者:turingH

為學大病在好名。

0x00 摘要

去年在分析CVE-2016-1757時,初步的接觸了MachIPC系統中使用的Message,在分析最近的一系列與IPC模塊相關的漏洞時,又加強對IPC模塊的理解,所以通過一到兩篇文章梳理一下最近的學習總結與心得體會。

關于CVE-2016-7637這個漏洞的描述有很多資料了,是一個攻擊Mach的內核IPC模塊的漏洞,本文最后會對漏洞做出比較詳細的解釋,這里給出幾個鏈接,不熟悉這個漏洞的讀者可以先了解一下。

黑云壓城城欲摧 - 2016年iOS公開可利用漏洞總結

mach portal漏洞利用的一些細節

Broken kernel mach port name uref

0x01 什么是Port

對于一般的開發者,在用到Port的時候可以簡單的將Port理解為進程間通信所使用的類似于Socket的東西,可以用他來發送消息,也可以用來接收消息。

下面我們來一步一步的構建對整個模塊的理解。

已經知道Port是什么的讀者可以跳過這一個部分。

1.1 利用Port傳遞數據

Port最簡單的可以理解為一個內核中的消息隊列。不同的Task通過這個消息隊列相互傳遞數據。而Port就是用于找到這個隊列的索引。

利用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
  • 在內核中代碼中,namepmach_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。

PORT_NAME_RIGHT

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;
}

很明顯,portname兩個變量都是由函數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的數據結構就較為簡單了。在復制給spaceie_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 */);
}

這里同樣的用書上的一張圖就可以很簡單的解釋清楚了。

ipc_port

0x02 POC的分析

? POC原來的writeup在這里

? 原文已經解釋的非常清楚了,我就不畫蛇添足了。簡單記錄一下我自己在分析的過程中的一些問題。

2.1 port的user reference計數代表了什么?

? 一個portuser reference只表示了某個entrytaskspace中被多少個地方使用,和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_deallocipc_entry_dealloc不會將entry對應的內存釋放,而是將其放入一個free_list等待重復使用。entry的內存不會釋放,而且entryis_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節提到的一樣,entrytable中的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_spaceipc_entry狀態變化。

接下來就要分析學習CVE-2016-7644,通過對CVE-2016-7644的分析學習,可以更加深入的理解port在內核中狀態的變化。也就是port自身的port->srightsio_reference的狀態變化及漏洞的利用。

參考

  1. 《Mac OS X Internals》

  2. Broken kernel mach port name uref


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/197/