w00w00!

lkm : Kernel hacking simplifié
Par : w00w00 Security Development article de Nicolas Dubee
Translation by eberkut - http://www.chez.com/keep

Ce qui suit s'applique à la série de kernel Linux i86 2.0.x. Il peut également être efficace pour les versions précédentes, mais n'a pas été testé. les kernel 2.1.x ont introduit beaucoup de modifications, notamment des routines de managment de mémoire, et ne sont pas étudiées ici.

Merci à Halflife qui a eu la première fois l'idée d'utiliser les lkm à des fins malveillants, et tiepilot, mon héros vivant.

Espace utilisateur vs. Espace kernel

Linux est un système d'exploitation protégé. Il est implémenté au-dessus du mode protégé de la série de CPU i386.

La mémoire est divisée en approximativement 2 parties : l'espace kernel et l'espace utilisateur. L'espace kernel est là où le code kernel vit, et l'espace utilisateur est là où les programmes utilisateur vivent. Naturellement, un programme utilisateur donné ne peut pas écrire dans la mémoire kernel ou dans la zone de mémoire d'un autre programme.

Malheureusement, c'est également le cas du code kernel. Le code kernel ne peut pas écrire dans l'espace utilisateur non plus. Qu'est ce que ça signifie ? Et bien, quand un driver matériel veut écrire des octets de données sur un programme dans la mémoire utilisateur, il ne peut pas la faire directement, mais à la place il doit utiliser des fonctions du kernel spécifiques. En outre, quand des paramaters sont passés par adresse vers une fonction kernel, la fonction kernel ne peut pas lire les paramètres directement. Elle doit employer d'autres fonctions kernel pour lire chaque octet des paramètres.

Voici quelques fonctions utiles à utiliser en mode kernel pour transféré des octets de données vers ou depuis la mémoire utilisateur.

#include <asm/segment.h>

get_user(ptr)
Donne l'octet, word, ou long donné par la mémoire utilisateur. C'est une macro, et elle se fonde sur le type de l'argument pour déterminer le nombre d'octets à transférer. Vous alors devez utiliser les typecasts sagement.

put_user(ptr)
C'est pareil que get_user(), mais au lieu de lire, il écrit des octets de données dans la mémoire utilisateur.

memcpy_fromfs(void *to, const void *from,unsigned long n)
Copie n octets depuis *from dans la mémoire utilisateur vers *to dans la mémoire kernel.

memcpy_tofs(void *to,const *from,unsigned long n)
Copie n octets depuis *from dans la mémoire kernel vers *to dans lé mémoire utilisateur.

System calls

La plupart des appels libc se fondent sur les appels système, qui sont les fonctions kernel les plus simples qu'un programme utilisateur peut appeler. Ces appels système sont implémenté au kernel lui-même ou dans des loadable kernel modules, qui sont de petits morceaux de code kernel dynamiquement liable.

Comme MS-DOS et beaucoup d'autres, les appels système Linux sont implémenté par un multiplexeur appelé avec une interruption masquable donnée. Dans Linux, cette interruption est int 0x80. Quand on exécute l'instruction 'int 0x80', la commande est donnée au kernel (ou, plus exactement, à la fonction _system_call()), et le processus réel de démultiplexage se produit.

* Comment fonctionne _system_call() ?

D'abord, tous les registres sont sauvegardés et la teneur du registre %eax est vérifiée avec la table globale d'appels système, qui énumère tous les appels système et leurs adresses.
Cette table peut être consultée avec la variable externe vide *sys_call_table[ ]. Une adresse mémoire et un nombre donnés dans cette table correspondent à chaque appel système. Les numéros d'appel système peuvent être trouvé dans /usr/include/sys/syscall.h. Ils sont de la forme SYS_systemcallname. Si l'appel système n'est pas implémenté, la cellule correspondante dans sys_call_table est 0, et une erreur est retournée. Autrement, l'appel système existe et l'entrée correspondante dans la table est l'adresse de mémoire du code d'appel système.

Voici un exemple d'un appel système invalide :


[root@plaguez kernel]# cat no1.c
#include <linux/errno.h>
#include <sys/syscall.h>
#include <errno.h>

extern void *sys_call_table[];

sc()
{ // L'appel système 165 n'existe pas à ce moment.
__asm__(
"movl $165,%eax
int $0x80");
}

main()
{
errno = -sc();
perror("test of invalid syscall");
}
[root@plaguez kernel]# gcc no1.c
[root@plaguez kernel]# ./a.out
test of invalid syscall: Function not implemented
[root@plaguez kernel]# exit


La commande est alors transférée à l'appel système réel, qui exécute celui que vous avez requis et retouné. _system_call() appelle alors ret_from_sys_call() pour contrôler diverse substance, et revient finalement à la mémoire utilisateur.

* libc

Le int $0x80 n'est pas utilisé directement pour des appels système ; on utilise plutôt des fonctions libc, qui sont souvent des wrappers pour interrompre 0x80.

le libc comporte généralement les appels système utilisant les macros de _syscallX(), où X est le nombre de paramètres pour l'appel système.

Par exemple, l'entrée de libc pour write(2) serait implémenté avec une macro _syscall3, puisque le prototype write(2) réel exige 3 paramètres. Avant d'appeler l'interruption 0x80, les macros _syscallX sont censés paramétrer stack frame et la liste d'argument exigées pour l'appel système.En conclusion, quand les retours de _system_call() (qui est déclenché avec int $0x80), les macros _syscallX() vérifieront une valeur de retour négative (dans le %eax) et placeront errno en conséquence.

Prenons un autre exemple avec write(2) et voyons comment il est prétraité.


[root@plaguez kernel]# cat no2.c
#include <linux/types.h>
#include <linux/fs.h>
#include <sys/syscall.h>
#include <asm/unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>

_syscall3(ssize_t,write,int,fd,const void *,buf,size_t,count);

main()
{
char *t = "this is a test.\n";
write(0, t, strlen(t));
}
[root@plaguez kernel]# gcc -E no2.c > no2.C
[root@plaguez kernel]# indent no2.C -kr
indent:no2.C:3304: Warning: old style assignment ambiguity in "=-". Assuming "= -"

[root@plaguez kernel]# tail -n 50 no2.C

#9 "no2.c" 2

ssize_t write(int fd, const void *buf, size_t count)
{
long __res;
__asm__ __volatile("int $0x80":"=a"(__res):"0"(4), "b"((long) (fd)), "c"((long) (buf)), "d"((long) (count)));
if (__res >= 0)
return (ssize_t) __res;
errno = -__res;
return -1;
};

main()
{
char *t = "this is a test.\n";
write(0, t, strlen(t));
}
[root@plaguez kernel]# exit


Notez que le "0"(4) dans la fonction write() ci-dessus correspond à la définition de SYS_write dans /usr/include/sys/syscall.h.

* Faites vos propres appels système.

Il y a quelques manière de faire vos propres appels système. Par exemple, vous pouvez modifier les sources du kernel et ajouter votre propre code. Une méthode bien plus facile, cependant, serait d'écrire loadable kernel module.

Un loadable kernel module n'est pas d'avantage qu'un fichier d'exécution contenant le code qui sera dynamiquement incorporé dans le kernel quand il est nécessaire.

Les buts principaux de ce dispositif sont d'avoir un petit kernel, et de charger un gestionnaire donné quand il est nécessaire avec la commande insmod(1). Il est également plus facile d'écrire un lkm que d'écrire le code dans l'arborescence des source du kernel.

* Écrire un lkm

Un lkm est facile à faire en C.
Il contient un gros morceau de #define, quelques fonctions, une fonction d'initialisation appelée init_module(), et une fonction de déchargement appelée cleanup_module().

Voici une structure typique de source de lkm :


#define MODULE
#define __KERNEL__
#define __KERNE_SYSCALLS__

#include <linux/config.h>
#ifdef MODULE
#include <linux/module.h>
#include <linux/version.h>
#else
#define MOD_INC_USE_COUNT
#define MOD_DEC_USE_COUNT
#endif

#include <linux/types.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/errno.h>
#include <asm/segment.h>
#include <sys/syscall.h>
#include <linux/dirent.h>
#include <asm/unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>

int errno;

char tmp[64];

/* for example, we may need to use ioctl */
_syscall3(int, ioctl, int, d, int, request, unsigned long, arg);

int myfunction(int parm1,char *parm2)
{
int i,j,k;
/* ... */
}

int init_module(void)
{
/* ... */
printk("\nModule loaded.\n");
return 0;
}

void cleanup_module(void)
{
/* ... */
}


Contrôlez les #define obligatoires (#define MODULE, #define __KERNEL__) et les #incldes (#include <linux/config.h >...)

Notez également que comme notre lkm fonctionnera en mode kernel, nous ne pouvons pas utiliser des fonctions libc, mais nous pouvons utiliser des appels système avec les macros précédemment étudiées de _syscallX().

Vous compilerez ce module avec 'gcc -c -O3 module.c' et l'insérerez dans le kernel avec 'insmod module.o' (l'optimisation doit être enclenchée).

Comme le titre le suggère, le lkm peut également être employé pour modifier le code kernel sans devoir le reconstruire entièrement. Par exemple, vous pourvez patcher l'appel système write(2) pour cacher des parties d'un fichier donné. Ca semble un bon endroit pour des backdoors, également : que feriez-vous si vous ne pouvez pas faire confiance à votre propre kernel ?

* Backdoors kernel et d'appels système

L'idée principale derrière tout ça est assez simple. Nous redirigerons ces fichus appels système vers les notres dans un lkm, qui nous permettra de forcer le kernel à réagir quand nous le voulons. Par exemple, nous pourrions cacher un sniffer en patchant l'appel système IOCTL et en masquant le bit PROMISC. Lame mais efficace.

Modifier un appel système donné, ajouter juste la définition du void externe *sys_call_table[] dans votre lkm, et prenez la fonction init_module() et modifiez l'entrée correspondante dans le sys_call_table pour pointer vers votre propre code. L'appel modifié peut alors faire tout ce que vous souhaitez, appellez l'appel système initial par la modification sys_call_table une fois de plus, et...