
/**************         ...: SysCalls Monitor SCM :...          ************** 
*                                                                            *
*  RedKod Team           visit us at : www.redkod.com      #redkod@undernet  *
*                                                                            *
******************************************************************************
*                                                                            *
*  Ce code est soumis aux termes de la GPL License, merci de les respecter.  *
*                                                                            *
*    Author       : ChiRon                  Release Date : 2002-07-19        *
*    Mail         : <chiron@fr.st>          Revision     : 2002-07-25        *
*    Filename     : scm.c                   Version      : 0.2               *
*                                                                            *
*****************************************************************************/

#define MODULE
#define __KERNEL__

#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/stddef.h>
#include <linux/sys.h>

#include <linux/time.h>
#include <linux/sched.h>
#include <linux/tqueue.h>
#include <linux/unistd.h>
#include <linux/string.h>
#include <linux/slab.h>

#include <sys/syscall.h>
#include <linux/file.h>
#include <asm/uaccess.h>

MODULE_AUTHOR("ChiRon <chiron@fr.st>");

/*
DEFAULT_DELAY   nombre par defaut de seconde(s) entre chaque verification
DEFAULT_CHECK   nombre par defaut de check de la table en une seconde par
                verification
MAX_SYSCALL     nombre du plus haut syscall utilise lors de l'utilisation de
                l'input system (vous pouvez le modifier, voyez votre
                asm/unistd.h mais restez prudent)
PREFIX          la sequence qui precede chaque nom de syscall dans le
                fichier asm/unistd.h
BYTES_MAX_LEN   nombre de bytes enregistres et verifies; plus de 10/15 bytes
                peut provoquer des segfault, alors soyez prudent
*/

#define PVERSION 0
#define SVERSION 2

#define DEFAULT_DELAY  10
#define DEFAULT_CHECK   1
#define MAX_SYSCALL    NR_syscalls - 1
#define PREFIX         "#define __NR_"
#define KMEM_ON        {mm_segment_t old_fs=get_fs();set_fs(KERNEL_DS);
#define KMEM_OFF       set_fs(old_fs);} 
#define ESCAPE_SPACE   {next_get=0; while(read_file(buffer, next_get) == next_get && is_sep(buffer[0])) { next_get=1; }; next_get=0;}

#define NAME_MAX_LEN   35
#define NUM_MAX_LEN     5
#define ADDR_MAX_LEN   10
#define BYTES_MAX_LEN  10
#define SYSCALL_LIST    0
#define SYSTEM_LIST     1
#define VERIFY_LIST     2
#define REPORT_LIST     3

#define ACTIVE          0
#define PASSIVE         1

#define OK              1
#define MODIFIED        2
#define RESTORED        3
#define MsgOK           "OK"
#define MsgRESTORED     "RESTAURE"
#define MsgMODIFIED     "MODIFIE"

#define TRUE            1
#define FALSE           0

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

MODULE_PARM(input,  "s");
MODULE_PARM(output, "s");
MODULE_PARM(action, "s");
MODULE_PARM(delay,  "i");
MODULE_PARM(check,  "i");


typedef struct sinfo_ {

  int num;                              /* numero du syscall */
  char name[NAME_MAX_LEN+1];            /* nom */
  unsigned char bytes[BYTES_MAX_LEN+1]; /* premiers bytes */
  unsigned int addr;                    /* adresse sous forme decimale */

  struct sinfo_ *next;

} sinfo;


struct queue {

  int taille;
  struct sinfo_ *head;
  struct sinfo_ *tail;

};


extern long *sys_call_table[];

static char *action, *input, *output; /* options de la ligne de commande */
static int delay = DEFAULT_DELAY, check = DEFAULT_CHECK, mode;

static int error_monitor = FALSE;
static int stop_monitor  = FALSE;
static int nb_check      = 0;
struct file *fichier     = NULL;
static int prefix_size;

struct queue system_liste;
struct queue actual_liste;
sinfo *system_tab[MAX_SYSCALL];

static struct timeval start_time, now_time;

/* I/O options */
int output_screen = FALSE,
    output_file   = FALSE,
    input_file    = FALSE;


#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
static struct tq_struct VerifyTask;
wait_queue_head_t WaitMonitor;
#else
void monitoring(void*);
static struct tq_struct VerifyTask = {
  NULL,
  0,
  monitoring,
  NULL
};
struct wait_queue *WaitMonitor;
#endif


void _memcpy(void *dest, const void *src, int size)
{
  int i;
  const char *s = src;
  char *d = dest;
  
  for (i=0;i<size;i++) *d++ = *s++;
}


int _memcmp(void *dest, const void *src, int size)
{
  int i, ret;
  const char *s = src;
  char *d = dest;
  
  for (i=0;i<size;i++)
    if ((ret = strncmp(d++, s++, 1)) != 0) return ret;

  return 0;
}


void _memprintk(void *src, int size)
{
  int i;
  char *s = src;
  
  for (i=0;i<size;i++) printk("%c", s[i]);
}


unsigned int my_atoi(char *buf)
{

  unsigned int i, nb=0;
  
  for(i=0;i<strlen(buf);i++) {
    nb  = nb*10;
    nb += buf[i] - '0';
  }
  
  return nb;
}


int is_digit(char c)
{
  int i;

  for(i='0';i<='9';i++)
    if (c == i)
      return 1;

  return 0;
}


int is_changeline(char c)
{
  if(c == '\r' || c == '\n')
    return 1;
  return 0;
}

 
int is_sep(char c)
{
  if(c == ' ' || c == '\t' || c == '\r' || c == '\n')
    return 1;
  return 0;
}


char* give_state(int state) {
  
  if (state == OK)
    return MsgOK;
  else if (state == MODIFIED)
    return MsgMODIFIED;
  else
    return MsgRESTORED;

  
}
 

void init_queue(struct queue *liste)
{
  
  liste->taille = 0;
  liste->head   = NULL;
  liste->tail   = NULL;
  
}


int del_queue(struct queue *liste)
{
  
  sinfo *next_del;
  sinfo *cur_del;
  
  if (liste == NULL || liste->taille == 0 || liste->head == NULL)
    return -1;
  
  for(cur_del=liste->head;cur_del!=NULL;cur_del=next_del) {
    next_del = cur_del->next;
    kfree(cur_del);
  }

  return 0;
  
}


int add_queue(struct queue *liste, char *name, int num, char *bytes, unsigned int addr)
{
  
  sinfo *cur_info;

  cur_info = (sinfo*) kmalloc(sizeof(sinfo), GFP_KERNEL);
  _memcpy(cur_info->name, name, strlen(name)+1);

  bytes[BYTES_MAX_LEN] = '\0';
  _memcpy(cur_info->bytes, (bytes == NULL ? "" : bytes), (bytes == NULL ? 1 : BYTES_MAX_LEN));

  cur_info->addr    = addr;
  cur_info->num     = num;
  cur_info->next    = NULL;
  if (liste->taille > 0)  liste->tail->next = cur_info;
  if (liste->taille == 0) liste->head = cur_info;
  liste->tail       = cur_info;
  liste->taille++;

  return 0;

}


void copy_queue(struct queue *src, struct queue *dest)
{

  sinfo *cur_info;

  for(cur_info=src->head;cur_info!=NULL;cur_info=cur_info->next)    
    add_queue(dest, cur_info->name, cur_info->num, cur_info->bytes, cur_info->addr);
  
}


int seek_queue(struct queue *liste, char* name, int num) 
{

  sinfo *cur_info;

  if(name != NULL)
    for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next)
      if (strcmp(name, cur_info->name) == 0) return 1;
  
  if(num > -1)
    for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next)
      if (cur_info->num == num) return 1;
  
  return 0;
}


int open_file(char *path, int mode, int perms)
{

  int error    = 0,
      old_uid  = current->uid,
      old_euid = current->euid;

  current->uid = current->euid = 0;

  fichier = filp_open(path, mode, perms);

  if (IS_ERR(fichier)) error = PTR_ERR(fichier);    
  else if (!S_ISREG(fichier->f_dentry->d_inode->i_mode))      error = -EACCES;
  else if (mode == O_WRONLY && fichier->f_op->write  == NULL) error = -EIO;
  else if (mode == O_RDONLY && fichier->f_op->read   == NULL) error = -EIO;

  if (error) {
    printk("Erreur durant l'ouverture du fichier %s\n", path);
    goto fin;
  }


fin:
  current->uid  = old_uid;
  current->euid = old_euid;
  return error;
}


int read_file(void *buf, int len)
{

  int r;

  KMEM_ON;
  r = fichier->f_op->read(fichier, buf, len, &fichier->f_pos);
  KMEM_OFF;

  return r;
}


int write_file(void *buf, int len)
{

  int r;

  KMEM_ON;
  r = fichier->f_op->write(fichier, buf, len, &fichier->f_pos);
  KMEM_OFF;

  return r;
}


int write_liste(char *path, struct queue *liste, int mode, int type_liste)
{

  char buffer[256];
  sinfo *cur_info;
  int nb_ecrits = 0, buffer_len, error = -1;

  if (open_file(path, mode, 0666) != 0)
    goto fclose;
  
  if (type_liste == VERIFY_LIST) {
 
    sprintf(buffer, "# Fichier genere par SCM v%d.%d GPL License - ChiRon 2002 - www.redkod.com\n"
	            "# Soyez prudent si vous l'editez manuellement !\n"
	            "# Pour eviter qu'un syscall soit verifie, placez un # au debut de sa ligne.\n#\n"
	            "# %-25s %-3s %-10s %d BYTES\n", PVERSION, SVERSION, "NOM", "NUM", "ADDRESSE", BYTES_MAX_LEN);
    write_file(buffer, strlen(buffer));

    for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next) {
      sprintf(buffer, "%-27s %3d %u ", cur_info->name, cur_info->num, cur_info->addr);
      buffer_len = strlen(buffer);
      _memcpy(buffer+buffer_len, cur_info->bytes, BYTES_MAX_LEN);
      buffer[buffer_len+BYTES_MAX_LEN] = '\n';
      write_file(buffer, buffer_len+BYTES_MAX_LEN+1);
    }

    printk("Le fichier \"%s\" a ete correctement genere.\n"
	   "N'oubliez pas pour plus de securite de le placer sur un peripherique etant\nPHYSIQUEMENT en lecture seule.\n", path);
    
  }
  else {
    
    sprintf(buffer, "# Fichier genere par SCM v%d.%d GPL License - ChiRon 2002 - www.redkod.com\n"
	            "# Rapport de Modification des Syscalls\n", PVERSION, SVERSION);
    write_file(buffer, strlen(buffer));

    for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next)
      if (cur_info->addr != OK || strcmp(cur_info->bytes, MsgOK) != 0) {
	sprintf(buffer, "%-27s %3d %-10s %-10s\n", cur_info->name, cur_info->num, give_state(cur_info->addr), cur_info->bytes);
	nb_ecrits++;
	write_file(buffer, strlen(buffer));
      }

    if (nb_ecrits == 0) {
      sprintf(buffer, "Aucune modification des Syscalls\n");
      write_file(buffer, strlen(buffer));
    }
  }
  
      
  error = 0;
  filp_close(fichier, NULL);
  
 fclose:
  return error;
}


void set_liste_to_tab(struct queue *liste)
{

  sinfo *cur_info;

  for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next)
    system_tab[cur_info->num]  = cur_info;

}


void get_syscall_info(sinfo *cur_info)
{
  
  _memcpy(cur_info->bytes, sys_call_table[cur_info->num], BYTES_MAX_LEN);
  cur_info->bytes[BYTES_MAX_LEN] = '\0';
  cur_info->addr = (unsigned int)sys_call_table[cur_info->num];

}

 
int file_get(char *buffer, char *dest, int *next_get, int next_end, int maxi, int (*fct_char)(char), int fct_retour)
{

  int len = 0;

  while(read_file(buffer, *next_get) == *next_get && (fct_char == NULL || fct_char(buffer[0]) == fct_retour)) {
    dest[len++] = buffer[0];
    *next_get = 1;
    if (len == maxi && fct_char == NULL) break;
    if (len > maxi) return -1;
  }
  dest[len] = '\0';
  *next_get = next_end;

  return 0;
}


int get_liste(char *path, struct queue *liste, int type_liste)
{

  char *window, *name, *num, *buffer, *bytes, *addr;
  int i, taille, next_get, error = -1;
  sinfo cur_info;

  if ((window = (char*) kmalloc(prefix_size+1, GFP_KERNEL))    == NULL) goto fwindow;
  if ((name   = (char*) kmalloc(NAME_MAX_LEN+1, GFP_KERNEL))   == NULL) goto fname;
  if ((num    = (char*) kmalloc(NUM_MAX_LEN+1, GFP_KERNEL))    == NULL) goto fnum;
  if ((addr   = (char*) kmalloc(ADDR_MAX_LEN+1, GFP_KERNEL))   == NULL) goto faddr;
  if ((bytes  = (char*) kmalloc(BYTES_MAX_LEN+1, GFP_KERNEL))  == NULL) goto fbytes;
  if ((buffer = (char*) kmalloc(1, GFP_KERNEL))                == NULL) goto fbuffer;

  window[prefix_size] = name[NAME_MAX_LEN] = num[NUM_MAX_LEN] = addr[ADDR_MAX_LEN] = bytes[BYTES_MAX_LEN] = '\0';

  if (type_liste != SYSTEM_LIST) {
    if (open_file(path, O_RDONLY, 0444))
      goto fclose;
    
    if ((taille = fichier->f_dentry->d_inode->i_size) == 0)
      goto fin;

    fichier->f_pos = 0;
  }

  error = 0;
  next_get = 1;

  if (type_liste == SYSTEM_LIST) {

    for(i=1;i<=MAX_SYSCALL;i++) {

      cur_info.num = i;
      get_syscall_info(&cur_info);

      if (!seek_queue(liste, NULL, i))
	add_queue(liste, "syscall", i, cur_info.bytes, cur_info.addr);

    }

  } else if (type_liste == SYSCALL_LIST) {

    while(read_file(buffer, next_get) == next_get) {

      next_get = 1;

      _memcpy(window, window+1, prefix_size-1);
      window[prefix_size-1] = buffer[0];

      if (strcmp(window, PREFIX) == 0) {

	if (file_get(buffer, name, &next_get, 0, NAME_MAX_LEN, is_sep, 0) == -1) goto fin;
	  ESCAPE_SPACE;
	  if (!is_digit(buffer[0])) continue;
	if (file_get(buffer, num, &next_get, 0, NUM_MAX_LEN, is_digit, 1) == -1) goto fin;

	if (!seek_queue(liste, name, my_atoi(num)) && my_atoi(num) >= 0 && my_atoi(num) <= MAX_SYSCALL)
	  add_queue(liste, name, my_atoi(num), "", 0);
      }
    }

  } else if (type_liste == VERIFY_LIST) {

    while(read_file(buffer, next_get) == next_get) {

      next_get = 1;

      if (buffer[0] == '#') { /* escape comments */
	next_get = 1;
	while (read_file(buffer, next_get) == next_get && !is_changeline(buffer[0]));
	continue;
      } else if ( ! is_sep(buffer[0])) {

	next_get = 0;

	if (file_get(buffer, name, &next_get, 0, NAME_MAX_LEN, is_sep, 0) == -1) goto fin;
	    ESCAPE_SPACE;
	if (file_get(buffer, num, &next_get, 0, NUM_MAX_LEN, is_digit, 1) == -1) goto fin;
	    ESCAPE_SPACE;
	if (file_get(buffer, addr, &next_get, 0, ADDR_MAX_LEN, is_digit, 1) == -1) goto fin;
	    ESCAPE_SPACE;
	if (file_get(buffer, bytes, &next_get, 1, BYTES_MAX_LEN, NULL, 0) == -1) goto fin;

      	if (!seek_queue(liste, name, my_atoi(num)) && my_atoi(num) >= 0 && my_atoi(num) <= MAX_SYSCALL)
	  add_queue(liste, name, my_atoi(num), bytes, my_atoi(addr));

      }
    }
  }


 fin:     if (type_liste != SYSTEM_LIST) filp_close(fichier, NULL);
 fclose:  kfree(buffer);
 fbuffer: kfree(bytes);
 fbytes:  kfree(addr);
 faddr:   kfree(num);
 fnum:    kfree(name);
 fname:   kfree(window);
 fwindow:

  return error;
}


void verify_system(struct queue *liste, struct queue *actual, int mode)
{

  sinfo *cur_info, *cur_system;
  int addr_state, bytes_state;
  long *real_addr;

  for(cur_info=actual->head;cur_info!=NULL;cur_info=cur_info->next)
    get_syscall_info(cur_info); /* recupere les actuels bytes et addresses */

  set_liste_to_tab(actual);     /* met *actual dans system_tab[] */

  for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next) {

    /* place les bytes des adresses originales sinon on compare les bytes des fonctions qui hijack, ceux-ci seront donc forcement differents et le test sera fausse */
    cur_system = system_tab[cur_info->num];
    (long)real_addr = cur_info->addr;
    _memcpy(cur_system->bytes, real_addr, BYTES_MAX_LEN);
    
    if (cur_system->addr != cur_info->addr)
      addr_state = MODIFIED;
    else
      addr_state = OK;

    if (_memcmp(cur_system->bytes, cur_info->bytes, BYTES_MAX_LEN))
      bytes_state = MODIFIED;
    else
      bytes_state = OK;
    
    if (mode == ACTIVE) {
      if (addr_state == MODIFIED) {
	(unsigned int)sys_call_table[cur_info->num] = cur_info->addr;
	addr_state = RESTORED;
      }

      if (bytes_state == MODIFIED) {
	_memcpy(sys_call_table[cur_info->num], cur_info->bytes, strlen(cur_info->bytes));
	bytes_state = RESTORED;
      }
    }


    cur_system->addr     = addr_state;
    cur_system->bytes[0] = '\0';
    strcpy(cur_system->bytes, give_state(bytes_state));

  }
}


void show_report(struct queue *liste, int mode, int type_liste) {

  sinfo *cur_info;

  if (output_screen)
    for(cur_info=liste->head;cur_info!=NULL;cur_info=cur_info->next)
      if (cur_info->addr != OK || strcmp(cur_info->bytes, MsgOK) != 0)
	printk("%-27s %3d %-10s %-10s\n", cur_info->name, cur_info->num, give_state(cur_info->addr), cur_info->bytes);

  if (output_file)
    if (write_liste(output, liste, mode, type_liste) == -1)
      printk("Erreur lors de l'ecriture dans le fichier \"%s\"\n", output);
}


void monitoring (void *bratisla_boyz)
{

  do_gettimeofday(&now_time);
    
  if (delay && (int)(now_time.tv_sec - start_time.tv_sec) % delay != 0) {
    nb_check = 0;
    goto fin;
  }

  if (nb_check >= check)
    goto fin;

  nb_check++;
  
  verify_system(&system_liste, &actual_liste, mode);
  show_report(&actual_liste, O_CREAT|O_APPEND, REPORT_LIST);
  
 fin:
  if (stop_monitor == TRUE)
    wake_up_interruptible(&WaitMonitor);
  else
    queue_task(&VerifyTask, &tq_timer);

}


int is_here_param(char *name, void *opt)
{

  if (opt == NULL || strlen(opt) == 0) {
    printk("Option manquante : %s\n", name);
    return 0;
  }

  return 1;
}


int is_ok_param(char *name, char *opt, char *arg)
{

  char *offset;

  do {

    if ((offset = strrchr(arg, ';')) == NULL)
      offset = arg;
    else {
      offset[0] = '\0';
      offset++;
    }

    if (offset[strlen(offset)-1] == ':') {
      if (strncmp(opt, offset, strlen(offset)-1) != 0)
	continue;

      if (strlen(opt) <= strlen(offset)) {
	printk("Argument non mentionne pour l'argument %s de l'option %s\n", offset, name);
	return 0;
      } else
	return 1;

    } else {
      if (strcmp(opt, offset) == 0)
	return 1;
    }

  } while (offset != arg);

  printk("Parametre inconnu pour %s : %s\n", name, opt);

  return 0;
}  


int init_module(void)
{

  sinfo *cur_info;
  char *set_mode;

  EXPORT_NO_SYMBOLS;  

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
  INIT_TQUEUE(&VerifyTask, monitoring, NULL);
#endif

  prefix_size = strlen(PREFIX);

  init_queue(&system_liste);
  init_queue(&actual_liste);

  if (!is_here_param("input",  input))   if (!is_here_param("output", output)) return 0;
  if (!is_here_param("action", action))  return 0;

  if (!is_ok_param("input",  input,  "system;file:"))  return 0;
  if (!is_ok_param("output", output, "screen;none;both:;file:")) return 0;
  if (!is_ok_param("action", action, "init;verify-active;verify-passive;monitor-active;monitor-passive")) return 0;

  if (delay < 0) delay = DEFAULT_DELAY;
  if (check < 0) check = DEFAULT_CHECK;

  if (strncmp(output, "both", 4) == 0 || strcmp(output, "screen") == 0)
    output_screen = TRUE;

  if (input[4] == ':') {
    input_file = TRUE;
    _memcpy(input, input+5, strlen(input)-5);
    input[strlen(input)-5] = '\0';
  }

  if ((set_mode = strchr(action, '-')) != NULL) {
    set_mode[0] = '\0';
    set_mode++;
    if (strcmp(set_mode, "active") == 0)
      mode = ACTIVE;
    else
      mode = PASSIVE;
  }

  if (output[4] == ':') {
    _memcpy(output, output+5, strlen(output)-5);
    output[strlen(output)-5] = '\0';
    if (strncmp(action, "monitor", 7) != 0)
      output_file = TRUE;
    else {
      error_monitor = TRUE;
      printk("L'action monitor ne peut prendre en _output_ ni l'option file ni l'option both !\n");
      return 0;
    }
  }
  
  if (strcmp(action, "init") == 0) {

    if (get_liste(input, &system_liste, (input_file ? SYSCALL_LIST : SYSTEM_LIST)) < 0) {
      printk("Impossible de recuperer les noms des appels systemes\n");
      return 0;
    }

    for(cur_info=system_liste.head;cur_info!=NULL;cur_info=cur_info->next)
      get_syscall_info(cur_info);

    if (output_screen)
      for(cur_info=system_liste.head;cur_info!=NULL;cur_info=cur_info->next) {
	printk("%-27s %3d %u ", cur_info->name, cur_info->num, cur_info->addr);
	_memprintk(cur_info->bytes, BYTES_MAX_LEN);
	printk("\n");
      }
    
    if (output_file)
      if (write_liste(output, &system_liste, O_CREAT|O_WRONLY|O_TRUNC, VERIFY_LIST) < 0)
	printk("Erreur lors de l'ecriture dans le fichier \"%s\"\n", output);

  }
  else if (strcmp(action, "verify") == 0) {

    if (get_liste(input, &system_liste, (input_file ? VERIFY_LIST : SYSTEM_LIST)) == -1) {
      printk("Impossible de recuperer la liste de verification\n");
      return 0;
    }

    init_queue(&actual_liste);
    copy_queue(&system_liste, &actual_liste);

    verify_system(&system_liste, &actual_liste, mode);
    show_report(&actual_liste, O_CREAT|O_APPEND, REPORT_LIST);

  }
  else if (strcmp(action, "monitor") == 0){
    
    if (get_liste(input, &system_liste, (input_file ? VERIFY_LIST : SYSTEM_LIST)) == -1) {
      printk("Impossible de recuperer la liste de verification\n");
      return 0;
    }
    
    init_queue(&actual_liste);
    copy_queue(&system_liste, &actual_liste);
   
    monitoring(NULL);

  }

  do_gettimeofday(&start_time);
  
  return 0;
}


void cleanup_module(void)
{
  
  if (strcmp(action, "monitor") == 0 && error_monitor == FALSE) { 
    
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
    init_waitqueue_head(&WaitMonitor);
#endif
    
    stop_monitor = TRUE;
    interruptible_sleep_on(&WaitMonitor);
    
  }

  del_queue(&system_liste);
  del_queue(&actual_liste);

}


#else /* LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) */

#warning "Votre noyau n'est pas assez recent pour permettre au module de fonctionner correctement.\nMettez-le a jour avec une version 2.2.0 ou ulterieure et ressayez.\nLa compilation est annulee.\n"

#endif /* LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) */

