因为项目需要,在Mac系统上要实现一个能监控所有的文件读写的应用,并且可以阻塞或者拒绝一些特定的文件操作。
在Window上可以通过文件系统过滤驱动来做,而且网上资料也挺多。
Mac平台上则需要通过内核扩展(KEXT)来做,相关的资料相对就少很多了,而且大部分是英文的,过程也中踩了一些坑。
就准备把流程在这篇文章里记录一下。
准备测试机
不管是Window还是Mac,估计Linux也一样,内核相关的开发是需要两台机器的。一台机器用于开发,一台机器专门用于测试编译出来的kext。
原因之一是方便,内核中如果出现空指针啊之类的错误的话,是会导致kernel panic,就只能重启系统了。普通的逻辑bug放到内核里也可能导致整个系统都不能正常运作,总之重启是家常便饭。这时两台机器分工能大大提高开发效率的。
原因之二是内核调试必须要使用两台机器。估计是因为内核断点下来之后,应用层的UI、调试器之类的程序也都暂停了。所以只能用另外的机器进行远程调试。
实际操作中通常是使用虚拟机,成本低,有快照支持,非常适合用来当测试用机。
虚拟化软件推荐直接使用VMware Fusion,最开始我使用的Virtual Box,想着是免费开源的,然而踩了几天坑之后,发现VBox对MacOS支持的并不好。
首先要创建安装系统用的iso:
1 | hdiutil create -o /tmp/HighSierra -size 8G -layout SPUD -fs HFS+J -type SPARSE |
第三行中的.app可以直接从App Store上下载,不同系统版本路径会稍有变化。
其他的命令是创建磁盘镜像,挂载,卸载,转换为iso格式,具体可以参考hdiutil的man文档。
然后就是新建虚拟机,把刚刚创建的iso装载到虚拟机光驱里,启动,安装就行了。没有什么需要特别设置的。
(VBox在安装过程中的第一次重启,不能引导正确的分区,需要进boot manager手动选择)
值得一提的是网卡这边可以设成host-only,这样就可以用虚拟机的ip直接访问它。但是虚拟机不能访问外网,不过也没必要访问外网。
通过打开虚拟机Mac自带的文件共享来传输文件,VMware自带的文件拖放不太好用。
配置调试环境
首先要给虚拟机安装上内核调试套件Kernel Debug Kit,这里面包含了Debug版的内核和驱动,以及相应的符号文件,可以从苹果的开发者中心下载。
注意版本号要与虚拟机里装的MacOS完全一致!
KDK的安装路径是/Library/Developer/KDKs,其中有一个官方的readme文件,KDK的使用方法,Debug方法基本都包含了,是非常有用的资料。
在虚拟机里完成KDK的安装后,需要设置启动参数,VMware里就跟真机一样设置就好了:
1 | sudo nvram boot-args="-v debug=0x146 kdp_match_name=en0 kcsuffix=development" |
- -v:输出所有内核日志,启动时也会输出到黑屏幕
- debug:内核调试的关键设置,具体可以参考苹果官方文档Building and Debugging Kernels里的Debugging flags
- kcsuffix:指定使用的内核,这里因为只是debug我们开发的kext,而不是内核本身,使用development的内核就可以了,性能会比较好。
- kdp_match_name:指定断点时用哪个网卡进行远程调试
这里特别提一下kdp_match_name这个选项,网上的教程大部分都没有这个选项。但是我最开始用VBox来远程调试的时候,死活都连不上,卡了很久,最后在前面提到的官方readme里找到了这个选项才解决。因为当时设置了两个网卡,除了host-only,还有一个NAT连外网,系统默认用了NAT的网卡来调试,当然就没办法连上了。
(VBox对nvram的支持非常差,直接用nvram命令设置是无效的,要用命令:VBoxManage setextradata 虚拟机名 “VBoxInternal2/EfiBootArgs” “-v debug=0x146 kdp_match_name=en0 kcsuffix=development”)
(后面关闭SIP的设置没法保存也是同理,必须每次启动都操作一次)
接下来要做的是替换development内核,但在此之前要先关闭SIP(System Integrity Protection),因为内核是受SIP保护的,而且后面要加载未签名的KEXT也是需要在SIP关闭的情况下。
- 重启,并按住Cmd+R进恢复模式
(VBox因为没有没有启动前的BIOS画面?所以也只能通过boot manager进恢复模式) - 在恢复模式的终端里执行csrutil disable
关闭了SIP后,重启,替换一下开发内核:
1 | sudo cp /Library/Developer/KDKs/KDK_10.14_18A391.kdk/System/Library/Kernels/kernel.development /System/Library/Kernels/kernel |
后两句是清除内核缓存并且重启。
到此调试环境就算是配置完成了。
KEXT开发调试
先来测试下远程调试是否OK。
因为前面设置了0x04这个debug标记,可以通过按键触发NMI (non-maskable interrupt)来进入调试状态。因为要按的键太多了,设置个映射比较方便。
按完之后如果虚拟机整个卡住了,那就说明进入调试状态了。
在开发机上也装上KDK,因为需要符号和内核调试用的lldb脚本。之后运行lldb进行调试:
1 | lldb /Library/Developer/KDKs/KDK_10.14_18A391.kdk/System/Library/Kernels/kernel.development |
按提示导入脚本:
也可以按提示设置一下自动导入,下次进来就不用再敲导入命令了。
用ip连接虚拟机进行调试:
1 | kdp-remote 192.168.19.128 |
剩下的就跟lldb调试普通应用差不多了,例如bt命令打印堆栈等等,这里就不展开了,大家可以单独去学习。
kext执行在内核中,调试也是差不多的。当你的kext引发了panic时,会自动断下来,此时用lldb连上去就能看到堆栈了。
也可以在lldb中下断点,或使用int3主动断下来。
最后我们来实现最开始说的文件读写监控的功能。
Google了一阵之后,发现苹果在10.4引入的Kauth(Kernel Authorization)相关KPI(Kernel Programming Interface),正好符合我们的要求。
Kauth里的Vnode Scope,会回调所有的文件系统操作给KEXT中注册的回调函数,包括读写、执行、删除等,并且可以由KEXT决定是否拒绝访问。
具体细节可以参考官方文档,同时还提供了一个示例代码,可以说是非常贴心了。虽然这些资料都有些年头了,但是完全没有过时。
接下来直接看代码:
1 | extern kern_return_t com_example_apple_samplecode_kext_KauthORama_start(kmod_info_t * ki, void * d); |
这是入口函数,在kext加载或卸载时执行,可以在工程设置中指定:
然后是注册回调函数,以及回调的部分实现:
1 | static void InstallVnodeListener() { |
更具体的代码就先略过了,可以参考官方文档和示例。除此之外还有用sysctl控制kext的方法,这个示例代码里就有。以及跟用户层进行类socket通信的KEXT Controls KPI。
代码写好之后,先编译出来,然后要在plist里配置一下依赖库,用kextlibs命令来查询用到了那些库:
填到plist的OSBundleLibraries项里:
重新编译一下就可以加载了,不加这些的话,在加载时就会出现找不到符号的错误。
在虚拟机上执行以下命令加载kext:
1 | chown -R root:wheel KauthORama.kext; kextload KauthORama.kext |
必须以root用户运行,我是先用了sudo -i进了root shell的,就不用每次都sudo了。chown的原因也是因为kextload的限制,要求kext文件的owner是root。
之后就可以通过log命令来查看kext里print出来的读写请求了:
1 | log show --last=3m --predicate 'sender == "KauthORama"' |
效果如下:
1 | Filtering the log data using "sender == "KauthORama"" |
当然也可以用控制台APP来查看,效果是一样的: