关于一个macOS 10.12.2到macOS 10.12.6内核崩溃问题的思索

首先申明的是这不是个利用,这个问题只能导致macOS 10.12.2~macOS 10.12.6内核崩溃。其次我曾在macOS 10.12.6发布的当天凌晨给苹果安全部门去信,这几天交流下来,苹果安全部门的口吻是这不是个安全问题,所以秉承着这仅仅是个BUG的原则,我们可以自由讨论这个BUG是怎么产生的以及影响如何。最后本人新人一枚,研究问题的能力有限,如果有大牛觉得这个问题能造成更深层次的问题,那么权当我抛砖引玉,感激不尽。本人也是第一次投稿,如有写错的地方请大家多多包涵。

我们先来看macOS 10.12.4(当前在opensource.apple.com公布的macOS的最新源码版本)IOPCIFamily的源代码,浏览至IOPCIBridge.c我们可以看到一个一直躺在那里的BUG:

当我们new一个IOPCIBridge且指定的type是kIOPCIDiagnosticsClientType时,实际new的是IOPCIDiagnosticsClient,我们再看这个实例在申请到内存后调用的initWithTask方法:

kIOClientPrivilegeAdministrator == root,所以我们用用户权限新建一个IOPCIBridge的client时,内核在调用initWithTask方法会返回false,即使我们使用root权限,PE_i_can_has_debugger判断失败也会返回false。

到这里其实都没有问题,问题出在由于先调用OSTypeAlloc申请了内存,并用uc指针指向这片内存,所以即使initWithTask返回false后,uc指针也是非空的,代码逻辑会去调用inPlane(gIOServicePlane)。这个函数继承自父类IORegistryEntry,我们来看源码:

据崩溃信息指向的就是这个函数,先贴出ida的结果:

实际崩溃在0xFFFFFF801A489D65 mov rax, [rdi]这一行,rdi==0×0000000000000000。瞧,我们发现是空指针的解引用导致了内核的崩溃。

那么是getParentSetReference引发的崩溃?……从ida结果来看,不知道是不是编译器做了什么奇怪的优化,0xFFFFFF801A489D6C这一行怎么看都不像是在调用IORegistryEntry::getParentSetReference这个函数,倒像是编译器把这个函数“inline”掉了。如果照此推断,我们继续看该函数的源码:

注意registryTable()这个函数,其实是个宏,我们来看定义:

我们再看IORegistryEntry类中对fRegistryTable的定义:

我们注意到这其实是IORegistryEntry类中的一个成员指针,而此变量只有在IORegistryEntry::init被调用后才会非空。

到此为止,如果倒推这个问题,那么所有的一切都能恍然大悟。还记得最一开始提到的IOPCIDiagnosticsClient::initWithTask这个函数。如果这个函数最终返回true,那么通过层层函数调用,IORegistryEntry::init将被调用,也就不会触发空指针解引用的问题。

那么接下来揣摩一下coder的思维逻辑。macOS 10.12.2以下并没有触发内核崩溃,我们浏览macOS 10.12.1的源码:

由于先做了判断,然后再去申请内存,所以uc == null的话就不会去调用inPlane,也就不会触发内核崩溃。macOS 10.12.2及以后coder可能想要优化代码,把判断挪进initWithTask中去了,却忘了要适当的修改代码。

最后说说空指针解引用能不能利用。苹果安全部门给我的答案是NO。OS X 10.10,10.11时代答案也许是YES,因为32位程序是允许map第一个page页的,虽然macOS 10.12也能map,但是测试下来似乎苹果已经加固了这部分缺陷。

如果想测试这个崩溃问题十分简单,苹果自己的源代码就可以测试(汗……苹果为什么不自己测试?),具体路径在IOPCIFamily/tools/pcidump.c。以彼之矛,攻彼之盾,祝大家崩溃day开心!