主页 > mac电脑教程 >

【原创】写一个简单的Mac内核反调试扩展

前言

在 Windows 上,用户直接操作的是应用层。在权限方面有很多限制。因此,大多数安全厂商和插件和木马都是内核级别的。看看谁最有机会接管内核级别。 ,最隐蔽。用到的技术有SSDT、Shadow SSDT Hook、DKOM、Inline Hook等,可以隐藏进程、隐藏端口、隐藏恶意文件、篡改用户行为、键盘网络监控等,之前也写过一些相关的技术文章博客。 Windows内核,有兴趣的可以看看。虽然这种对抗在Mac平台上很少见,但我们也可以开发自己的内核扩展模块来隐藏进程、隐藏文件、监控网络。

环境准备

cp /Library/Developer/KDKs/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development /Systems/Library/Kernels

sudo nvram boot-args="debug=0x141 kext-dev-mode=1 kcsuffix=development pmuflags=1 -v"

debug=0x141:这里可以查到意思,这里是(DB_HALT | DB_ARP | DB_LOG_PI_SCRN)

kext-dev-mode=1: 允许加载未签名的 kext

kcsuffix=development:指定加载上面复制的kernel.development

pmuflags=1:关闭定时器

-v:显示内核加载信息

settings set target.load-script-from-symbol-file true

然后运行以下命令开始内核调试:

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_tsend,
477 __unused kdp_receive_treceive)
-> 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,可以通过以下方式直接创建内核扩展模块。

创建的项目会有以下两个函数,在驱动加载和卸载时调用。它们一般用于加载时初始化设备驱动程序和钩子函数,卸载时恢复钩子,删除设备驱动程序等:

#include <mach/mach_types.h>
#include  //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中,所以必须包含头文件。另外,为了在加载时找到符号,在Info.plist中的OSBundleLibraries中添加com.apple.kpi.libkern16.0.0。这个版本号可以通过在被调试机器上运行 kextstat 来查看。在Build Setting中使用DSYM File将Debug Information Format设置为DWARF,即Debug中也会生成符号文件,方便源码调试。为了让调试器主动中断,在加载模块时使用int 3触发断点。

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

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

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

sudo dmesg | tail -n 10

以及加载时会主动触发断点的本地符号文件对应的源码对应行如下:

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系统服务地址。可以通过修改数组的函数地址来到达钩子。在XNU中,还有一个系统调用表叫做sysent,它的定义可以在bsd/sys/sysent.h中找到:

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_tsy_return_type; /* system call return types */
int16_tsy_narg;/* number of args */
uint16_tsy_arg_bytes;/* Total size of arguments in bytes for
* 32-bit system calls
*/
};

所以如果能找到这个表在内存中的位置vm mac进代码调试模式,然后修改表中ptrace和sysctl对应的sy_call,就可以接管ptrace和sysctl的调用了,但是因为这个符号没有导出,所以可以只能从内核文件分析中找到。然后将模块在内存中加载的基地址添加到文件中的相应位置,以确定 sysent。首先使用IDA打开文件/Library/Developer/KDKs/KDK_10.12_16A323.kdk/System/Library/Kernels/kernel.development 搜索并在__constdata处找到符号sysent,并根据IDA手动显示结果对上述结构的分析如下图所示:

您可以通过搜索 sysent 表中前几个符号的地址来确定 __constdata 中 sysent 的位置。因为KASLR,加载后必须先在内存中找到内核模块的基地址,可以通过10.11 vm_kernel_unslide_or_perm_external 计算偏移后vm mac进代码调试模式,才能在内存中查找文件头:

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,在这部分找到sysent的位置:

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 默认为只读,因此在写入之前必须关闭写保护。完成后恢复:

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,然后使用如下代码测试效果:

#import 
#import 
#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调试:

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

本文主要是根据目前的一些文章和代码整理的。笔者自己的尝试是了解Mac下kext开发的过程。以同样的方式人们可以一起交流。

【培训】优秀毕业生寄语:恭喜id的咸鱼炒白菜拿到远超3W月薪的offer。 “Android进阶培训班”招生啦! ! !