AFFICHER CET ARTICLE EN MODE PLEINE PAGE
Sommaire
2) Sys_open & sys_close démistifiés
__2.1 Ouvrir un fichier depuis le "kernel-land"
__2.2 Fermer un fichier depuis le kernel land
3) Sys_read & sys_write démistifiés
__3.1. Ecriture dans un fichier depuis le kernel land
__3.2. La lecture de fichier depuis le kernel-land
5) Création de la fonction kernel_write
En userland, l'ouverture, la lecture, l'écriture et la fermeture de fichiers se fait en C (sous unix) grace aux primitives open(2), read(2), write(2), close(2). Dans le kernel land, c'est différent. Lorsque l'on programme un lkm, par exemple un sniffer de mots de pass, il peut être intéressant d'avoir recourt à des fichiers pour stocker des données (les mots de pass dans notre cas).
Pour cela, nous devons étudier le comportement des différents appels systèmes qui sont appelés par les primitives vue ci-dessus ( ex: SYS_open ). Avant de me lancer dans les sources du noyau, j'ai d'abord recherché des informations sur le net et j'ai trouvé un lkm ( le keylogger de rd [1] ) et quelques papiers ( un de mammon [2] et de Owen Klan [3] ) qui m'ont aidé à écrire cet article.
2) SYS_open & SYS_close démistifiés
Pour utiliser un fichier, il faut d'abord l'ouvrir. En userland, cette ouverture se fait grace à la fonction open(2) qui prend comme argument l'emplacement du fichier, le mode d'ouverture ( O_APPEND, O_WRONLY ... ), puis l'umask si le fichier doit être créé. Cette fonction fait appel à l'appel système SYS_open qui est déclaré dans fs/open.c comme suit:
asmlinkage long sys_open(const char * filename, int flags, int mode) _#if BITS_PER_LONG != 32 _out_error; |
Cet appel commence par appeler la fonction getname() pour obtenir une copie du chemin du fichier dans l'espace kernel. Si aucune erreur se produit, un appel a get_unused_fd() est réalisé pour obtenir un descripteur de fichier (fd) qui n'est pas utilisé. Ce descripteur sera renvoyé au programme qui à fait appel à open(). Ensuite on a un appel la fonction filp_open(). Cette fonction ne fait pas grand chose, elle initialise quelques variables puis fait appel à une autre fonction open_namei() puis dentry_open() qui s'occupent de l'ouverture à proprement dite du fichier. Ces deux fonctions ne sont pas très utiles pour l'écriture d'un lkm puisque pour ouvrir un fichier, il suffira de faire appel à filp_open() ( prototype dans linux/fs.h ) qui prend exactement les mêmes arguments que open() mais qui retourne un pointeur sur une structure file ( prototype dans linux/fs.h ) mais il est quand même interressant de regarder ce qu'elles font.
Regardons d'abord la structure file qui regroupe les informations qui vont servir pour les prochaines opérations sur le fichier ( lecture, écriture ... ):
struct file _unsigned long f_version; /* needed for tty driver, and maybe others */ /* Used by fs/eventpoll.c to link all the hooks to this file */ |
Les champs les plus importants sont f_dentry qui pointe sur le répertoire dans lequel le fichier ( ouvert ) est situé, f_pos qui contient l'actuelle position dans le fichier ( utile pour la lecture et l'écriture ), f_op qui pointe sur la structure file_operations qui contient une liste de pointeurs qui eux pointent sur des fonctions pour réaliser des opérations sur le fichier ouvert (ex: f_op->write pour écrire dans le fichier).
int open_namei(const char * pathname, int flag, int mode, struct nameidata *nd); |
La fonction open_namei définit dans fs/namei.c éffectue des routines de vérification sur les flags, crée le fichier si nécessaire, puis remplit la structure nameidata.
struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags) |
La fonction dentry_open fait le reste du travail, elle remplit les champs de la structure file ( ex: f_pos est mis à 0 ) puis la retourne. Il reste alors à créer le fd qui sera retourné à l'user-land avec la fonction fd_install() On peut maintenant lire et écrire dans le fichier. Voilà pour l'appel system SYS_open qu'on peut résumer à ce shéma:
-> SYS_open(); |
Fermer un fichier depuis l'espace kernel se fait grace à la fonction filp_close() définit dans fs/open.c. En faite l'appel système SYS_close vérifie si le fichier est ouvert et remplit la structure file puis fait appel à filp_close() avec comme paramètres un pointeur sur cette structure et le "thread ID" pour les fichiers attachés a un processus, sa valeur est NULL lorsqu'il n'est pas attaché à un processus.
int filp_close(struct file *filp, fl_owner_t id); |
On sait maintenant ouvrir et fermer un fichier avec un lkm. Voici un petit exemple avec gestion des erreurs:
#include <linux/fs.h> |
IS_ERR() est un macro qui vérifie ici que l'ouverture du fichier s'est bien déroulé, il transforme (cast) juste en unsigned long et le compare à -1000 si la valeur de f est plus grande que -1000 alors il y a eu une erreur.
#define IS_ERR(ptr) ((unsigned long)(ptr) > (unsigned long)(-1000)) |
Il nous reste maintenant à lire ou à écrire dans un fichier depuis l'espace kernel.
3) SYS_write & SYS_read démistifié
Nous sommes maintenant capable d'ouvrir et de fermer un fichier depuis un lkm grace aux fonctions filp_open et filp_close. L'écriture et la lecture sont toutes aussi faciles.
Quand un programme fait appel à la fonction write(2), c'est l'appel système sys_write qui est définit dans fs/read_write.c qui est appelé:
asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count) _ret = -EBADF; |
La structure file est remplit grâce à la fonction fget() qui prend comme argument le fd envoyé par l'espace utilisateur. Une vérification sur cette structure est ensuite réalisée pour vérifier si l'on a les droits en écriture sur le fichier. Ensuite on fait appel à une autre fonction write():
write(file, buf, count, &file->f_pos); |
Cette fonction est très importante puisque c'est elle qui va écrire réellement dans le fichier, elle ressemble à celle de l'espace utilisateur, elle prend 4 arguments, un sur la struct file (file), un pointeur sur une chaine de caratères à écrire dans le fichier (buf), un int qui spécifie le nombre d'octets à écrire dans le fichier (count), puis la position actuel dans le fichier (&file->f_pos) où seront écrit les données. Pour écrire des données dans un fichier, on peut se limiter à l'appel de cette fonction. On peut même définir un macro comme la fait rd dans son keylogger:
#define _write(f, buf, sz) (f->f_op->write(f, buf, sz, &f->f_pos)) |
Différents problèmes peuvent être rencontrés si on appel directement cette fonction (write()). Ces problèmes peuvent être résolus en changeant la valeur de current->addr_limit et la mettre à KERNEL_DS (0xffffffff) pour pouvoir accèder au 4Go d'espace mémoire. Ce changement (sur i386) peut être réalisé à l'aide des fonctions set_fs et get_fs. Ainsi on aura un code du genre:
mm_segment_t saved_fs; saved_fs = get_fs(); /* sauvegarde */ write(...); set_fs(saved_fs); /* comme avant write() */ |
Voilà on sait écrire dans un fichier depuis le "kernel-land". Nous verons un exemple plus tard.
La lecture de fichier depuis l'espace kernel est tout à fait identique à l'écriture vue précédement. La fonction read(2) fait appelle au kernel par l'intermédiaire de l'appel système sys_read définit dans fs/read_write.c.
asmlinkage ssize_t sys_read(unsigned int fd, char * buf, size_t count) _ret = -EBADF; |
Tout comme sys_write, la structure file est remplit puis une vérification sur les droits ( en lecture ) sur le fichier ouvert est réalisée. Le fichier est ensuite bloqué pour éviter des problèmes. Ensuite, après quelques vérifications, sys_read appel la fonction read() ( celle de l'espace kernel ). C'est évidément cette fonction qui va réaliser la lecture: read(file, buf, count, &file->f_pos);. Elle possède les mêmes arguements que la fonction write() (3.1), c'est à dire un pointeur qui pointe sur la structure file, un pointeur qui pointe sur une chaine de caratère ou seront stocké les éléments lus (buf), le nombre d'octets à lire (count) et enfin la position actuel dans le fichier (&file->f_pos).
Comme avec write(), on peut réaliser un macro du genre:
#define _read(f, buf, sz) (f->f_op->read(f, buf, sz, &f->f_pos)) |
Les problèmes que l'on peut rencontrer avec write() sont également disponibles avec read() donc il faut également utiliser set_fs pour positionner current->addr_limit à KERNEL_DS.
Voilà vous savez maintenant ouvrir, lire, écrire et fermer un fichier depuis l'espace kernel (pour un exemple complet rendez-vous au 'chapitre' 6) mais ce n'est pas tout, les développeurs du kernel ont créé une fonction kernel_read() qui permet de lire directement et sans problèmes des fichiers depuis le kernel-land mais ils ont oublié la fonction kernel_write(), ce n'est pas grave nous allons l'a faire.
En fouillant un peu dans les sources situées dans le répertoire fs/, on peut remarquer que dans exec.c il y a une fonction (kernel_read) qui pourrait peut être nous être utile,
int kernel_read(struct file *file, unsigned long offset, char * addr, unsigned long count) _if (!file->f_op->read) |
Son prototype peut être trouvé dans linux/fs.h. On retrouve dedans tout ce que l'on a besoin (cité plus haut) pour lire un fichier depuis le kernel-land, en effet elle positionne le flag KERNEL_DS dans current->addr_limit grace aux fonctions set_fs et get_fs pour éviter les problèmes, puis elle appel la fonction read(). Elle prend comme argument un pointeur sur une structure file, un unsigned long int sur l'offset dans le fichier où l'on doit commencer la lecture, un pointeur sur une chaine de caratères pour stocker ce qui a été lu et un unsigned long int qui indique le nombre d'octets à lire dans le fichier. Et voilà avec une seule fonction on peut lire un fichier depuis l'espace kernel, sans se soucier des problèmes.
On peut définir un macro comme celui-ci:
#define kread(f, buf, sz) (kernel_read(f, sz, buf, &f->f_pos)) |
Il reste maintenant plus qu'a trouver une fonction kernel_write() :).
5) Création de la fonction kernel_write()
Comme le sous-entend le titre, il n'existe pas de fonction kernel_write(), du moins sur les kernels de la branche 2.4. On va donc la créer pour le fun en se basant sur kernel_read.
Voici la fonction commentée:
static int kernel_write(struct file *file, unsigned long offset, char *addr, unsigned long count) _old_fs = get_fs(); /* sauvegarde de la valeur de current->addr_limit */ |
Voilà c'était pas dur, elle ressemble énormément à la fonction kernel_read(), elle prend exactement les mêmes arguments et elle procède de la même manière que celle-ci. On peut réaliser un patch pour le kernel, il suffit de rajouter cette fonction dans fs/exec.c par exemple et son prototype dans linux/fs.h. Un exemple de patch se trouve en annexe.
Comme exemple, j'ai décidé de faire le plus simple possible. En effet ce lkm ne fait que copier le contenue du début de la première ligne du fichier /etc/shadow dans /tmp/.pass. Si vous avez compris ce qui a été dit plus haut et un minimum de connaissances sur les lkms vous n'aurez aucun problème pour comprendre ce code:
#define MODULE #define SIZE 20 /* root:pass + 1 */ #include <linux/config.h> MODULE_LICENSE("GPL"); struct file *fd; int init_module(void) _fd = filp_open("/etc/shadow", O_RDONLY, 0); _old_fs = get_fs(); _return 0; void cleanup_module(void) |
Voilà vous savez (normalement) maintenant lire et écrire dans un fichier depuis l'espace kernel. J'ai écris ce papier puisque je devais réaliser un lkm qui sniffait les login/pass ftp (je l'ai pas encore finit d'ailleurs!) et je pense que ça peut être utile à d'autres personnes.
[1] rd "Writing Linux Kernel Keylogger" Phrack Issue 0x3b, Phile #0x0e.
[2] mammon "Reading Files into Linux Kernel Memory"
[3] Owen Klan "Using files in kernel code"
[*] Linux kernel source. ( fs/open.c fs/exec.c fs/read_write.c ... )
[*] Man pages. ( read(2), write(2), open(2), close(2) )
/*--------------------------------kernel_write.patch----------------------------- */ int kernel_write(struct file *file, unsigned long offset, char *addr, unsigned long count) _extern int kernel_read(struct file *, unsigned long, char *, unsigned long); |
BY VIRIIZ
Copyright © 2005 ARENHACK - DHS