动手写一个简单的Mac内核反反调试扩展

前言

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lldb /Library/Developer/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development
(lldb) kdp-remote 虚拟机IP地址
.........
Process 1 stopped
* thread #2, name = '0xffffff800b777b00', queue = '0x0', stop reason = signal SIGSTOP
frame #0: 0xffffff800520ea14 kernel.development`kdp_register_send_receive(send=(IONetworkingFamily`IOKernelDebugger::kdpTransmitDispatcher(void*, unsigned int) at IOKernelDebugger.cpp:369), receive=(IONetworkingFamily`IOKernelDebugger::kdpReceiveDispatcher(void*, unsigned int*, unsigned int) at IOKernelDebugger.cpp:353)) at kdp_udp.c:478 [opt]
475 kdp_unregister_send_receive(
476 __unused kdp_send_t send,
477 __unused kdp_receive_t receive)
-> 478 {
479 if (current_debugger == KDP_CUR_DB)
480 current_debugger = NO_CUR_DB;
481 kdp_flag &= ~KDP_READY;
Target 0: (kernel.development) stopped.
(lldb)

编写内核扩展

环境准备好之后就可以开始编写和调试内核扩展了,编写的过程还是在自己的系统完成然后在拷贝到虚拟机里面所以最好保证主系统和虚拟机系统大版本一致,首先使用Xcode就可以通过如下方式直接创建内核扩展模块。

创建出来的工程中会有如下两个函数,分别是驱动加载和卸载时调用的函数,一般用于加载的时候初始化设备驱动、hook函数,卸载的时候还原hook、删除设备驱动等等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <mach/mach_types.h>
#include <libkern/libkern.h> //printf所在头文件
kern_return_t HelloWorld_start(kmod_info_t * ki, void *d);
kern_return_t HelloWorld_stop(kmod_info_t *ki, void *d);
kern_return_t HelloWorld_start(kmod_info_t * ki, void *d)
{
asm("int $3");
printf("HelloWorld_start...\n");
return KERN_SUCCESS;
}
kern_return_t HelloWorld_stop(kmod_info_t *ki, void *d)
{
printf("HelloWorld_stop...\n");
return KERN_SUCCESS;
}

分别在上面两个函数中使用printf打印输出,在kext中printf函数是在libkern中的,所以要include该头文件。另外为了在加载的时候能找到符号还要在Info.plist中的OSBundleLibraries加上com.apple.kpi.libkern 16.0.0,后面这个版本号可以在被调试的机器上面运行kextstat看到, 在Build Setting设置Debug Information FormatDWARF with DSYM File 即在Debug也生成符号文件方便源码调试, 为了让调试器主动断下来便在模块加载的时候使用int 3主要触发断点。

使用scp ./HelloWorld.kext xxx@10.xx.xx.xx:/Users/xxxx/Desktop/将生成的HelloWorld.kext文件拷贝到被调试的机器,在这之前需要在被调试机器上的系统偏好设置-共享开启远程登录。最后使用如下命令修改用户组并加载以及卸载:

1
2
3
4
sudo chown -R root:wheel ./HelloWorld.kext
sudo kextload ./HelloWorld.kext
sudo kextutil ./HelloWorld.kext //可以查看加载出错的原因
sudo kextunload ./HelloWorld.kext

使用如下命令可以查看在模块中的输出日志:

1
sudo dmesg | tail -n 10

并且在加载的时候会主动触发断点并关联到本地的符号文件对应的源码的对应行如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Loading 1 kext modules . done.
Process 1 stopped
* thread #17, name = '0xffffff801a9be1a0', queue = '0x0', stop reason = EXC_BREAKPOINT (code=3, subcode=0x0)
frame #0: 0xffffff7f95c61f21 HelloWorld`HelloWorld_start(ki=0xffffff7f95c62000, d=0x0000000000000000) at HelloWorld.c:17
14
15 kern_return_t HelloWorld_start(kmod_info_t * ki, void *d)
16 {
-> 17 asm("int $3");
18 printf("HelloWorld_start...\n");
19 return KERN_SUCCESS;
20 }
Target 0: (kernel.development) stopped.
(lldb)

内核反调试

之前在关于反调试&反反调试那些事中讲到一些反调试的方法和过反调试的方法,但是在应用层的话需要对每个应用都进行patch hook,而通过内核扩展模块可以直接从内核模块来接管系统调用针对调试检测和禁止附加的系统调用进行修改就行了。

我们知道在Windows平台有一个叫SSDT的东西,SSDT 的全称是 System Services Descriptor Table,系统服务描述符表,里面保存 Windows 系统服务地址的数组,通过修改该数组的函数地址就可以达到hook的效果,而在XNU中也有一个系统调用的表叫sysent,在bsd/sys/sysent.h可以找到其定义:

1
2
3
4
5
6
7
8
9
10
11
struct sysent { /* system call table */
sy_call_t *sy_call; /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
sy_munge_t *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
int32_t sy_return_type; /* system call return types */
int16_t sy_narg; /* number of args */
uint16_t sy_arg_bytes; /* Total size of arguments in bytes for
* 32-bit system calls
*/
};

所以如果能够找到这个表在内存中的位置然后修改其中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
2
3
4
m_offset_t func_address = (vm_offset_t) vm_kernel_unslide_or_perm_external;
vm_offset_t func_address_unslid = 0;
vm_kernel_unslide_or_perm_external(func_address, &func_address_unslid);
g_kernel_slide = func_address - func_address_unslid;

得到内核加载在内存中的偏移之后剩下就是macho文件格式的解析与处理,首先获取__CONST,__constdata并该section中找sysent所在的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct sysent * find_sysent_table(){
if (g_sysent_table) {
return g_sysent_table;
}
mach_header_t* kernel_header = find_kernel_header();
if (!kernel_header) {
return NULL;
}
// The first three entries of the sysent table point to these functions.
sy_call_t *nosys = (sy_call_t *) kernel_find_symbol("_nosys");
sy_call_t *exit = (sy_call_t *) kernel_find_symbol("_exit");
sy_call_t *fork = (sy_call_t *) kernel_find_symbol("_fork");
if (!nosys || !exit || !fork) {
return NULL;
}
const char *data_segment_name;
const char *const_section_name;
if (macOS_Sierra()) {
data_segment_name = "__CONST";
const_section_name = "__constdata";
} else {
data_segment_name = "__DATA";
const_section_name = "__const";
}
section_t* section = macho_find_section(kernel_header, data_segment_name, const_section_name);
if(!section){
return NULL;
}
vm_offset_t offset;
for (offset = 0; offset < section->size; offset += 16) {
struct sysent *table = (struct sysent *) (section->addr + offset);
if (table->sy_call != nosys) {
continue;
}
vm_offset_t next_entry_offset = sizeof(struct sysent);
if (OSX_Mavericks()) {
next_entry_offset = sizeof(struct sysent_mavericks);
}
struct sysent *next_entry = (struct sysent *)
((vm_offset_t)table + next_entry_offset);
if (next_entry->sy_call != exit) {
continue;
}
next_entry = (struct sysent *)
((vm_offset_t)next_entry + next_entry_offset);
if (next_entry->sy_call != fork) {
continue;
}
g_sysent_table = table;
return g_sysent_table;
}
return NULL;
}

获取到sysent之后就可以直接保存原来的sy_call然后替换sy_call的地址为我们自己的实现就可以接管系统调用了,由于__CONST,__constdata默认是只读的所以写之前要先关闭写保护写完之后再还原:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kern_return_t anti_ptrace(int cmd){
/* Mountain Lion (10.8+) moved sysent[] to read-only section */
kwrite_on();
/*
* we check if the syscalls had been already assigned, because we get kernel panic if we overwrite the syscall with same function
*/
if(cmd == DISABLE && g_sysent_table[SYS_ptrace].sy_call != (sy_call_t *)sys_ptrace){
if(sys_ptrace != NULL){
/* restore pointer to the original function */
g_sysent_table[SYS_ptrace].sy_call = (sy_call_t *)sys_ptrace;
/* remove the flag that indicates the hooked status */
g_hooked_functions &= ~HK_PTRACE;
}
}else if(cmd == ENABLE && !(g_hooked_functions & HK_PTRACE)){
/* save address of the real function */
sys_ptrace = (void *)g_sysent_table[SYS_ptrace].sy_call;
/* hook the syscall by replacing the pointer in sysent */
g_sysent_table[SYS_ptrace].sy_call = (sy_call_t *)ar_ptrace;
/* we set our global variable g_hooked_functions to know this function has been hooked */
g_hooked_functions |= HK_PTRACE;
}
kwrite_off();
return KERN_SUCCESS;
}

最后编译加载kext然后使用如下代码测试效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#import <dlfcn.h>
#import <sys/sysctl.h>
#ifndef PT_DENY_ATTACH
#define PT_DENY_ATTACH 31
#endif
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);
BOOL isDebuggerPresent(){
int name[4];
struct kinfo_proc info;
size_t info_size = sizeof(info);
info.kp_proc.p_flag = 0;
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();
if(sysctl(name, 4, &info, &info_size, NULL, 0) == -1){
return NO;
}
return ((info.kp_proc.p_flag & P_TRACED) != 0);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);
ptrace_ptr_t ptrace_ptr = (ptrace_ptr_t)dlsym(handle, "ptrace");
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);
NSLog(@"pass ptrace");
if(isDebuggerPresent()){
return 0;
}
NSLog(@"pass sysctl");
}
return 0;
}

使用lldb调试:

1
2
3
4
5
6
lldb ./demo
(lldb) target create "./demo"
Current executable set to './demo' (x86_64).
2017-11-18 14:06:22.370273 demo[403:4277] pass ptrace
2017-11-18 14:06:22.370346 demo[403:4277] pass sysctl
Process 403 exited with status = 0 (0x00000000)

相关代码: MacKext

AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!