古人学问无遗力,少壮功夫老始成。纸上得来终觉浅,绝知此事要躬行。
—陆游《冬夜读书示子聿》
0x00 摘要
本文是第三篇基于漏洞分析来学习Mach IPC
的方面知识的记录。
阅读顺序如下。
1.再看CVE-2016-1757—浅析mach message的使用
3.从CVE-2016-7644回到CVE-2016-4669(本文)
CVE-2016-7644这个漏洞,本身是一个很简单的漏洞,但是通过一些技巧,可以做一些更有意思的事情。
poc
与writeup
在这里。
CVE-2016-4669的POC,之前我已经分析过了,详见CVE-2016-4669分析与调试。在做完这一系列的IPC
相关的漏洞研究与学习之后,尝试的对CVE-2016-4669这个漏洞实现一个提权的利用。
并不能稳定触发,不过也加深了对内核的内存布局与IPC模块的理解。代码在这里。
0x01 CVE-2016-7644 POC分析
漏洞的成因并不复杂,当两个线程同时调用时,ipc_port_release_send
函数可能会被调用两次。
1 | kern_return_t |
在ipc_port_release_send
函数内会修改port
的一些属性,因为两个线程同时调用,触发了并发的漏洞,导致了bug
的发生。
1 | void |
POC
代码编译成功之后,利用shell循环执行就可以触发内核崩溃,因为是并发的漏洞,所以需要尝试的次数比较多,手动执行可能很难触发。
0x02 mach_portal_redist 相关利用代码分析
通过阅读mach_portal_redist
项目的kernel_sploit.c
文件,漏洞的利用总共分为以下几个部分。
- 获取内核中指向
port
的野指针 - 堆内存的布局
- 通过
UAF
获取kernel port
想要完全理解这几个部分,就需要了解更多的IPC
相关的知识。
2.1 利用流程
通过漏洞获取一个指向port
的野指针,流程大致如下。
1 | //申请port |
2.2 stash_port
当代码执行到[1]处时,做好了所有触发漏洞前的准备。我们的port
拥有3对right
和reference
。
通过mach_port_allocate
函数的执行,task
拥有port的一份right
和reference
。
通过set_dp_control_port
函数的执行,dynamic_pager_control_port
拥有port的一份right
和reference
。
这两个部分比较容易理解,stash_port
的原理较为复杂,利用了IPC
系统通过mach message
传递消息时的特性。
在通过message
传递一个port right
时的流程大致如下。
1 | /* |
当代码通过IPC
系统发送一个port right
时,port
的right
和reference
都会加1,而在读取消息时,会把right
和reference
减1,所以在未调用free_stashed_ports
读取出message
之前,就在IPC
系统中存放了一份port
的引用。
2.3 mach_port_deallocate
调用mach_port_deallocate
函数可以释放目标port
的一个RIGHT
。我们的port
的reference
为3,sright
是2。
2.4 race
race
就是利用了set_dp_control_port
函数的漏洞,在并发执行的时,会导致对dynamic_pager_control_port
连续两次调用ipc_port_release_send
函数。
ipc_port_release_send
每执行一次,会对目标port
的sright
和reference
做出一次减一的操作。这个时候我们的port
的reference
变成了1,而sright
变成了0,因为没有sendright
存在了,所以会产生一个notify
,通过这个特性,我们就可以知道我们成功的出发了条件竞争的漏洞了。
2.5 free_stashed_ports
在stashed_ports_q
的消息队列中还保存着我们传递的port
,只需要对stashed_ports_q
调用mach_port_destroy
,因为传递的port
的reference
已经是1了,在处理这个逻辑之后,port
在内核中就已经被释放了,而我们的task
中还保存了一个dangling
的port
。
1 | /* |
调用栈大致如上所示,最核心的逻辑在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
这个zone
中free element
。 - [b]是已经被使用的
element
,且16个字节都使用到了。 - [c]是已经被使用的
element
,但是只用前面八个字节,所有后面8个字节是0xdeadbeefdeadbeef
。
因为漏洞会越界访问,对下个element
中的地址调用ipc_port_release_send
,所以通过向一个很多的stash port
,发送同一个target port
的right
,在发送完成后再释放其中一部分得stash port
,在kalloc.16
的zone
中制造触发漏漏洞的时候使用的free element
。
在构造完成后大致如下:
3.2 触发漏洞
这里要把patch
的参数个数从1改成2。
1 | #if UseStaticTemplates |
出发漏洞后,就可以看到内存布局。
简单的调试流程如下:
先找到mach_ports_register
函数第一次调用ipc_port_release_send
的地方,并下一个断点。
1 | dis -n mach_ports_register [...] 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 [...] |
1 | (lldb) b *0xffffff800b0e22bc Breakpoint 1: where = kernel`mach_ports_register + 524 at ipc_tt.c:1097, address = 0xffffff800b0e22bc |
然后执行exp
程序,一般情况下是第二次命中断点时,portsCnt=3
(第一次命中时portsCnt=1
并不是我们的代码触发的,可以不管)。内存布局如下:
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 |
接着,mach_ports_register
的逻辑就会越界将0xffffff80115cfa00
处的0xffffff8013dee0e0
拷到task->itk_registered
中去。
1 | for (i = 0; i < TASK_PORT_REGISTER_MAX; i++) { |
通过lldb
查看ports
的状态。
1 | p *(ipc_port_t)0xffffff8013dee0e0 |
在第二次调用到mach_ports_register
的时候,会对他们调用ipc_port_release_send
。
1 | //第二次调用mach_ports_register函数时,ports中的数据变成了刚刚存储的 |
这个时候再观察port
在内核中的状态,机会发现ip_srights
和io_references
都做了一次减一。
这个时候在释放掉所有的stashed port
就将我们的target port
释放掉了。因为通过触发bug
多释放了一次。
- 通过
stashed port
创造了4096个reference
,释放掉所有的stashed port
就对reference
做了4096次减一,通过触发bug 又多做了一次release
,释放了一开始mach_port_allocate
创建的reference
。 srights
与reference
相同。
3.3 重用port
这一步在我的EXP
里就是看脸了,成功率并不是很高,没有找到稳定的利用方法。就不多说什么了,从别的EXP
里抄来的代码。
0x04 小结
到这里整个MACH-IPC
相关的漏洞分析与学习就暂时告一段落了。
这篇分析日志,断断续续写了很久,可能思路有点不连贯,有什么问题欢迎大家一起探讨:)