前言
在Windows上面用户直接操作的东西都是在应用层在权限方面有很多限制,所以大部分的安全厂商和外挂、木马的对抗都是在内核层,看谁在内核层接管的时机最早,隐蔽性最高。使用到的技术有SSDT、Shadow SSDT Hook、DKOM、Inline Hook等等来达到隐藏进程、隐藏端口、隐藏恶意文件、篡改用户行为、键盘网络监控等等、之前的博客也有写过一些相关的技术文章Windows内核,感兴趣的可以看看。而在Mac平台虽然这种对抗现在很少,但是我们也可以开发自己的内核扩展模块来达到隐藏进程、隐藏文件、监控网络等操作。
环境准备
安装虚拟机和系统: 在编写Windows内核驱动的时候要需要准备环境,当时是通过Windows安装VM虚拟机加上Windbg来调试内核以及内核驱动的。所以Mac内核的调试也可以按照这个套路,笔者的环境是VMware Fusion加macos 10.12。首先安装VMware Fusion这个没什么好说的,然后下载macos 10.12的系统镜像文件下载地址,下完安装并设置网络为桥接模式。
安装KDK: 为了调试dev的内核首先要在mac电脑和虚拟机里面的系统安装苹果提供的Kernel Debug Kit,首先要找到调试系统的版本号,点击左上角的苹果->关于本机->系统报告->软件->系统版本,笔者的是macOS 10.12(16A323)。直接在这里找到对应的版本下载然后分别在自己的系统和调试系统安装KDK。
关闭SIP: 由于后面需要运行未签名的内核扩展所以需要先关闭SIP,在虚拟机开机的时候按Command+R进入恢复模式(要多试几次),然后在终端输入
csrutil disable
。顺便把KDK里面的开发内核拷贝命令如下:
1 | cp /Library/Developer/KDKs/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development /Systems/Library/Kernels |
- 设置boot-args: 为了将虚拟机设置成调试模式,需要使用
nvram
设置boot-args
,命令如下:
1 | sudo nvram boot-args="debug=0x141 kext-dev-mode=1 kcsuffix=development pmuflags=1 -v" |
debug=0x141: 含义可以在Kernel Programming Guide找到,这里使用的是(DB_HALT | DB_ARP | DB_LOG_PI_SCRN)
kext-dev-mode=1: 允许加载未签名kext
kcsuffix=development: 指定加载上面拷贝的kernel.development
pmuflags=1: 关闭定时器
-v: 显示内核加载信息
- 清除kext缓存: 先运行
uanme -v
查看xnu源码版本,然后运行sudo kextcache -invalidate /
和sudo reboot
清除内核缓存使用新的内核加载然后重启,重启之后被调试的系统会显示IP等待被调试器附加如下图:
- 配置源码进行调试: 为了能够在调试的时候能对应到源码,从苹果开源代码下载上一步看到的xnu版本的代码放到
/Library/Caches/com.apple.xbs/Sources/xnu/
下面。并将如下设置加到~/.lldbinit
文件:
1 | settings set target.load-script-from-symbol-file true |
然后运行如下命令就可以开始内核调试:
1 | lldb /Library/Developer/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development |
编写内核扩展
环境准备好之后就可以开始编写和调试内核扩展了,编写的过程还是在自己的系统完成然后在拷贝到虚拟机里面所以最好保证主系统和虚拟机系统大版本一致,首先使用Xcode就可以通过如下方式直接创建内核扩展模块。
创建出来的工程中会有如下两个函数,分别是驱动加载和卸载时调用的函数,一般用于加载的时候初始化设备驱动、hook函数,卸载的时候还原hook、删除设备驱动等等:
1 | #include <mach/mach_types.h> |
分别在上面两个函数中使用printf打印输出,在kext中printf函数是在libkern中的,所以要include该头文件。另外为了在加载的时候能找到符号还要在Info.plist
中的OSBundleLibraries
加上com.apple.kpi.libkern 16.0.0
,后面这个版本号可以在被调试的机器上面运行kextstat
看到, 在Build Setting
设置Debug Information Format
为DWARF with DSYM File
即在Debug也生成符号文件方便源码调试, 为了让调试器主动断下来便在模块加载的时候使用int 3
主要触发断点。
使用scp ./HelloWorld.kext xxx@10.xx.xx.xx:/Users/xxxx/Desktop/
将生成的HelloWorld.kext
文件拷贝到被调试的机器,在这之前需要在被调试机器上的系统偏好设置-共享开启远程登录。最后使用如下命令修改用户组并加载以及卸载:
1 | sudo chown -R root:wheel ./HelloWorld.kext |
使用如下命令可以查看在模块中的输出日志:
1 | sudo dmesg | tail -n 10 |
并且在加载的时候会主动触发断点并关联到本地的符号文件对应的源码的对应行如下:
1 | Loading 1 kext modules . done. |
内核反调试
之前在关于反调试&反反调试那些事中讲到一些反调试的方法和过反调试的方法,但是在应用层的话需要对每个应用都进行patch hook,而通过内核扩展模块可以直接从内核模块来接管系统调用针对调试检测和禁止附加的系统调用进行修改就行了。
我们知道在Windows平台有一个叫SSDT的东西,SSDT 的全称是 System Services Descriptor Table,系统服务描述符表,里面保存 Windows 系统服务地址的数组,通过修改该数组的函数地址就可以达到hook的效果,而在XNU中也有一个系统调用的表叫sysent,在bsd/sys/sysent.h
可以找到其定义:
1 | struct sysent { /* system call table */ |
所以如果能够找到这个表在内存中的位置然后修改其中ptrace和sysctl在表中对应的sy_call就可以接管ptrace和sysctl的调用了,但是因为这个符号不导出只能从内核文件分析找到在内存在模块加载的基地址然后加上文件中对应的位置确定sysent。首先使用IDA打开文件/Library/Developer/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development
搜索找到符号sysent
位于__constdata
处,根据上面的结构体将IDA显示结果手动解析如下图所示:
可以通过搜索sysent表中前面几个符号的地址来确定sysent在__constdata
的位置,由于KASLR所以首先要找到内核模块加载后在内存中的基地址,在10.11之后可以通过vm_kernel_unslide_or_perm_external
来计算偏移,之前的话可以在内存中搜索文件头:
1 | m_offset_t func_address = (vm_offset_t) vm_kernel_unslide_or_perm_external; |
得到内核加载在内存中的偏移之后剩下就是macho文件格式的解析与处理,首先获取__CONST,__constdata
并该section中找sysent
所在的位置:
1 | struct sysent * find_sysent_table(){ |
获取到sysent
之后就可以直接保存原来的sy_call
然后替换sy_call
的地址为我们自己的实现就可以接管系统调用了,由于__CONST,__constdata
默认是只读的所以写之前要先关闭写保护写完之后再还原:
1 | kern_return_t anti_ptrace(int cmd){ |
最后编译加载kext然后使用如下代码测试效果:
1 | #import <dlfcn.h> |
使用lldb调试:
1 | lldb ./demo |
相关代码: MacKext