22 août 2008

    Linux Security Modules

    Catégorie : Sécurité     Tags :      

    Retrouvez cet article dans : Linux Magazine Hors série 17

    Historique
    Lors du Kernel Summit de San José, en mars 2001, Peter Loscocco présenta Security Enhanced Linux [SELinux], un patch pour le noyau Linux implémentant une architecture de contrôle d'accès, tous deux développés par la NSA.
    SE Linux n'était pas le seul correctif de la sorte. D'autres projets comme RSBAC, Medusa DS9, LIDS, SubDomain, DTE ou encore LoMAC [rsbac, medusa, lids, subdomain, dte, lomac] implémentent d'autres contrôles d'accès ou d'autres charpentes de contrôle d'accès.
    Linus Torvalds, conscient de la nécessité de permettre d'autres types de contrôles d'accès en standard, autres que ceux déjà présents (1), et plutôt que de privilégier un type de contrôle d'accès sur tous les autres, décida alors qu'il serait prêt à intégrer dans la distribution standard du noyau 2.6 une charpente commune laissant chacun choisir la méthode de contrôle d'accès de son choix.

    (1)c'est-à-dire le Discretionary Access Control (DAC) (les droits UNIX que tout le monde connaît) et les capabilities).

    Ainsi naquit le projet Linux Security Modules (LSM). La charpente prit la forme d'un ensemble de points de contrôle (hook ou ancre) dans le noyau Linux, afin de brancher différents types de contrôles d'accès. On y trouve actuellement une implémentation de SE Linux, LIDS, DTE, et même les capabilities ont été sorties du noyau pour s'appuyer sur les LSM.
    Il est à noter que le mot module dans LSM ne signifie pas que le contrôle d'accès doit être implémenté sous forme de module noyau (LKM), mais que la charpente est assez bien faite pour que tout code de contrôle d'accès l'utilisant soit modulaire. On peut ainsi le compiler sous forme de module si on le désire, mais rien ne nous y oblige.

    Quelques mots sur le contrôle d'accès

    Lorsqu'on s'intéresse à la sécurité d'un système, il est nécessaire de répondre à de nombreuses questions. Qui est qui ? Qui fait quoi ? Qui peut faire quoi (et quand) ? etc. Le contrôle d'accès est la réponse à certaines de ces questions en renforçant certaines contraintes de sécurité, comme la confidentialité, l'intégrité ou la disponibilité.

    Modèle de sécurité

    Pour cela, le contrôle d'accès apporte des solutions précises et fiables. Comme son nom l'indique, le contrôle d'accès cherche à déterminer qui peut accéder à quoi sur un système, et donc définir ce qu'on entend par "qui" et "quoi" :

    • qui : un sujet est une entité active sur le système, comme un processus agissant pour le compte d'un utilisateur ;
    • quoi : un objet est une entité passive sur le système, comme un fichier.

    Selon les cas, il est possible qu'un même élément d'un système soit un sujet ou un objet. Si on appelle une commande permettant de prendre l'identité d'un autre utilisateur (su), alors l'utilisateur qui appelle su sera un sujet, mais l'identité cible un objet.
    Enfin, il reste à définir ce qu'on entend par « accéder ». Les privilèges (ou permissions ou droits) constituent le dernier élément intervenant dans le contrôle d'accès. Les droits auxquels nous sommes habitués sous Unix (read, write, execute) ne sont qu'un exemple parmi tant d'autres.
    Un modèle général de sécurité est présenté en figure 1 :

    • l'authentification a pour objectif de garantir l'identité du demandeur qui présente des credentials : un mot de passe (je sais), une Smartcard (je possède), la biométrie (je suis) ;
    • une fois la légitimité du demandeur reconnue, sa requête pour accéder à un objet est confrontée à la base des autorisations et au contrôle d'accès ;
    • l'audit est le mécanisme qui supervise tout ce qui se passe, enregistre les événements afin de déterminer les tentatives de violations de la politique de sécurité (offline après les faits, ou en temps réel pour de la détection d'intrusion).

    /img-articles/lmhs/17/art-2/fig-1.jpg

    Fig. 1 : Contrôle d'accès

    Ainsi, des sujets (ex: utilisateurs) détiennent des privilèges sur des objets (ex: fichiers, devices) en accord avec une politique (ex. : modèle de Biba) de contrôle d'accès mise en œuvre  par le reference monitor.

    Politiques et mécanismes

    Le contrôle d'accès détermine qui est autorisé ou non à accéder à quoi. Cette distinction entre autorisation et rejet est effectué en adéquation avec une politique de contrôle d'accès.
    Le contrôle d'accès est un problème délicat, en particulier lorsqu'on compare plusieurs sujets :

    • il existe des rôles différents : utilisateurs normaux vs super-utilisateur ;
    • il existe des groupes ou hiérarchie : étudiants vs professeurs ;
    • il y a besoin de déléguer ses droits et pouvoirs.

    Pour tenter de simplifier la résolution de ces problèmes, on distingue la politique de sécurité (Discretionary Access Control - DAC, Mandatory Access Control - MAC, Role-Based Access Control - RBAC), des mécanismes (matrice d'accès, liste de contrôle d'accès - ACL, liste de capability) mises en œuvre pour appliquer la politique de sécurité.
    Le mécanisme de contrôle d'accès le plus complet est la matrice d'accès [Lampson74]. Pour un système, il s'agit d'identifier tous les sujets (lignes) et tous les objets (colonnes), puis de définir les relations qui les unissent. Bien évidemment, une telle matrice est énorme et très creuse, ce qui la rend très peu pratique. En fait, cette matrice sera représentée et utilisée sous forme de liste :

    • Access Control List (ACL) : il s'agit des colonnes de la matrice de contrôle d'accès, c'est-à-dire que pour chaque objet du système, on conserve la liste des utilisateurs qui peuvent y accéder et des opérations qu'ils peuvent effectuer dessus ;
    • Capability List : il s'agit des lignes de la matrice de contrôle d'accès, c'est-à-dire que pour chaque sujet du système, on conserve la liste des objets auxquels ils peuvent accéder et des opérations qu'ils peuvent effectuer dessus.

    La politique de contrôle d'accès la plus simple est celle qui est laissée à discrétion de l'utilisateur, d'où son nom Discretionary Access Control. Il s'agit par exemple de celle à laquelle chaque utilisateur d'Unix est habitué, avec les droits en lecture, écriture et exécution, qu'il peut changer à souhait sur les objets qu'il possède. Toutefois, une telle politique possède d'importants défauts. Tout d'abord, la politique globale de sécurité peut être compromise par un seul utilisateur s'il commet un erreur. Ensuite, le flux d'information n'est pas du tout contrôlable : un utilisateur capable de lire des données peut ensuite les transmettre à un utilisateur qui n'était pas censé les voir.
    C'est pourquoi on préfère en général d'autres modèles, plus évolués mais un peu moins flexibles.
    Le Role-Based Access Control (RBAC) est une autre forme de contrôle d'accès. Avec la matrice d'accès, on considère uniquement des sujets et des objets. Ici, on met en place des classes d'équivalence qui regroupent plusieurs sujets (on parle de groupe ou de rôle). Cela est particulièrement utile lorsque les sujets changent fréquemment car on évite ainsi de recalculer la matrice.
    Le modèle de rôles le plus utilisé est celui proposé dans [SC96]. Il s'agit en fait d'une "indirection" entre les sujets et les permissions, illustrée en figure 2 : les sujets n'ont pas directement de droits, ce sont les rôles qui en ont mais les sujets sont affectés à des rôles. Des modèles de RBAC proposent également des hiérarchies de rôles, et des contraintes sur les rôles.

    /img-articles/lmhs/17/art-2/fig-2.jpg

    Fig. 2 : RBAC basique

    Enfin, Le Mandatory Access Control (MAC) s'appuie sur l'utilisation de labels associés aux objets et sujets :

    • le label d'un sujet (clearance ou accréditation) indique le niveau de confiance de ce sujet ;
    • le label d'un objet spécifie le niveau minimal que doit avoir un sujet pour accéder à l'objet en question.

    Un cas particulier de MAC est donné par les politiques de sécurité multi-niveaux. Définissons par exemple 4 labels (top secret, secret, confidentiel et non classifié), ainsi que 2 règles qui seront systématiquement appliquées :

    • read down : l'accréditation d'un sujet doit dominer le niveau de sécurité de l'objet qu'il lit ;
    • write up : l'accréditation d'un sujet doit être dominée par le niveau de sécurité de l'objet qu'il écrit.

    De cette manière, on garantit (mathématiquement) la confidentialité des objets en fonction des niveaux d'accréditation. Ce modèle, élaboré en 1975, s'appelle Bell-La Padula (BLP) [BLP73], du nom de ses concepteurs. Le modèle "inverse" (read up, write down, modèle de Biba [Biba77]), garantit quant à lui l'intégrité des données.
    À la différence des permissions habituelles sous Unix, ici, l'utilisateur n'est plus maître même des objets qu'il possède. Ainsi, il est possible de baisser les capacités de root, comme nous le verrons dans l'exemple en fin d'article.
    Il existe de nombreux autres modèles de contrôle d'accès, chacun avec ses objectifs. Lorsque Linus Torvalds a voulu que la sécurité entre dans la noyau, il ne voulait néanmoins pas imposer un modèle précis, chacun ayant ses propres besoins. Ainsi, les LSMs constituent non pas un modèle de contrôle d'accès, mais une charpente, dans laquelle chacun peut brancher ce qu'il veut.
    Signalons que les LSM ne constituent pas une nouveauté. De telles Charpentes existaient déjà par ailleurs, comme le Generalized Framework for Access Control (GFAC) de RSBAC [rsbac], ou le Flux Advanced Security Kernel (FLASK, [flask]) implémenté, entre autres, par SE Linux.

    Survol de la charpente LSM

    La charpente en elle-même ne fait que rajouter des points de contrôle aux endroits critiques dans le code du noyau, ainsi que des champs destinés à contenir des informations de sécurité dans les structures internes du noyau. Aucune sécurité n'est ajoutée. Toute la logique du contrôle d'accès réside dans le module de sécurité qui sera utilisé. Nous verrons plus tard qu'il est possible, dans certaines conditions, de charger plusieurs modules les uns derrière les autres.
    Anecdotiquement, le patch LSM enlève également toute la logique de contrôle associée aux capabilities du noyau pour la mettre dans un module de sécurité utilisant la charpente.

    Points de contrôle

    Un point de contrôle se présente sous la forme d'un appel à une fonction dont l'adresse est stockée dans une structure pointée par la variable globale security_ops. Cette structure est du type security_operations :

    struct security_operations {
            int (*sethostname) (char *hostname);
            int (*setdomainname) (char *domainname);
            int (*reboot) (unsigned int cmd);
            [...]
    };

    Chaque module de sécurité fournit une structure de ce type. Lorsqu'il est chargé, il s'enregistre pour faire pointer la variable security_ops vers sa structure référençant ses propres fonctions de contrôle d'accès.
    On voit ici le code rajouté par le patch LSM pour appeler la fonction de contrôle d'accès.

    asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void * arg
    )
     {
            char buffer[256];
    +       int retval;
    
            /* We only trust the superuser with rebooting the system. */
            if (!capable(CAP_SYS_BOOT))
                    return -EPERM;
    
    +       retval = security_ops->reboot(cmd);
    +       if (retval) {
    +               return retval;
    +       }
    +
            /* For safety, we require "magic" arguments. */
            if (magic1 != LINUX_REBOOT_MAGIC1 ||
                (magic2 != LINUX_REBOOT_MAGIC2 && magic2 != LINUX_REBOOT_MAGIC2A &&

    Dans la version pour les noyaux 2.5/2.6, le point de contrôle se présente sous la forme d'un appel à une fonction inline qui se charge de déréférencer security_ops et d'appeler la fonction du module de sécurité utilisé. Ce double appel est équivalent à la version des noyaux 2.4 après optimisation par le compilateur, et est plus propre :

    static inline int security_bprm_set (struct linux_binprm *bprm)
    {
            return security_ops-bprm_set_security (bprm);
    }
    Et on trouvera donc au lieu de 
    
        retval = security_ops-bprm_set_security (bprm);
    le code suivant : 
    
        retval = security_bprm_set(bprm);

    Les points de contrôle ne sont pas ajoutés au hasard dans le code. En fait, ils se situent systématiquement "au même endroit", c'est-à-dire après tout un tas de vérifications préalables. Par exemple, dans le cas de la fonction vfs_symlink(), on commence par vérifier le droit de créer le lien symbolique dans le répertoire destination. Ensuite seulement, on appelle le hook du LSM. Enfin, on crée le lien symbolique, sous réserve que l'autorisation ait été accordée (cf partie "champ de sécurité" pour voir le code complet de cette fonction).
    Cela a plusieurs conséquences. Tout d'abord, cela implique que les arguments reçus par les points de contrôle sont déjà présents en espace noyau et des contrôles d'erreur ont déjà été effectués. Ce sont autant de choses qui ne sont plus à faire. Ensuite, il faut bien comprendre que les tests du LSM arrivent dans les derniers : si un seul des tests précédents échoue, on quitte la fonction avant d'avoir le temps de demander l'avis du LSM. Un point de contrôle ne peut donc pas surcharger une décision antérieure (ni postérieure, d'ailleurs).
    Enfin, dernier point mais non le moindre, les points de contrôle sont restrictifs (restrictive), c'est-à-dire que par défaut, ils considèrent que l'accès est valide. Leur rôle est de déterminer les situations dans lesquelles l'accès doit être empêché. C'est pourquoi, en regardant le module par défaut security/dummy.c vous constaterez que tous les points de contrôle renvoient 0, ce qui signifie qu'ils ne s'opposent pas à la demande, et donc l'accès à l'objet est autorisé.

    Champs de sécurité

    Pour mettre en œuvre une politique de sécurité, il est souvent nécessaire de conserver des données, servant généralement à prendre les décisions sur l'accès à l'objet. Par exemple, dans le modèle de BLP vu précédemment, chaque objet possède un label, son niveau de sécurité. La solution la plus simple est alors de prévoir pour chaque objet du noyau un champs (un void *) destiné à contenir ces informations.

    Objet Structure
    Programme
    struct linux_binprm
    Processus
    struct task_struct
    Système de fichiers
    struct super_block
    Fichier, pipe, socket, inode
    struct inode et struct file
    Paquet et device réseau
    struct sk_buff et struct net_device
    IPC
    struct kern_ipc_perm et struct  msg_msg

    Tab. 1 : Structure contenant un champ de sécurité en fonction du type d'objet

    Le champ de sécurité se présente donc sous forme d'un pointeur, initialisé à NULL (cf Tab. 1). Il faut donc prévoir des points de contrôle spécifiques destinés à la gestion de ces pointeurs : allocation de mémoire et initialisation, libération, modification, etc. Pour cela, il existe des hooks spécifiques pour chaque structure contenant un champ de sécurité, appelés *_alloc_security() et *_free_security() (respectivement pour allouer et libérer les champs de sécurité), où * correspond à la structure. Ces fonctions retournent 0 en cas de réussite, et prennent comme argument la structure à laquelle on souhaite attacher une structure de sécurité.
    Prenons comme exemple la structure inode (include/linux/fs.h). Elle contient un champ supplémentaire destiné à la sécurité :

    struct inode {
            ...
            void          *i_security;
            ...
    };

    Parmi les pointeurs sur fonction contenus dans la security_ops, on trouve ceux destinés à gérer ce champ, soit pour la structure inode :

    int (*inode_alloc_security) (struct file * inode);

    Les concepteurs des LSMs ont prévu dans le code du noyau des points d'entrée qui permettent au programmeur d'un module de sécurité spécifique d'allouer le champ de sécurité :

            struct *inode;
    
            ...
            error = security_ops->inode_alloc_security(inode);
            if (!error) {
                    ...
            }

    Toutefois, s'il est indispensable d'allouer ces champs de sécurité, il l'est tout autant de pouvoir les mettre à jour. Pour le moment, les points de contrôle que nous avons évoqués intervenaient dans la chaîne de contrôle d'accès pour autoriser ou non l'accès à un objet. Pour remédier à cela, des hooks <objet>_post_<operation> sont prévus, où <objet> est un objet du système (file, inode, task, etc.) et <operation> ... une opération sur l'objet en question.
    Revenons à notre objet inode. Il existe une fonction de contrôle associée à la création d'un lien symbolique inode_symlink(). Dans le cas où la création du lien est autorisée et se passe sans erreur, une seconde fonction de contrôle permet de mettre à jour le champ de sécurité : inode_post_symlink(). On trouve alors dans le code du noyau :

    int vfs_symlink(struct inode *dir, struct dentry *dentry, const char *oldname)
    {
    	int error;
    
     	down(&dir->i_zombie);
    	error = may_create(dir, dentry);
    	if (error)
    		goto exit_lock;
    
    	error = -EPERM;
    	if (!dir->i_op || !dir->i_op->symlink)
    		goto exit_lock;
    
    	error = security_ops->inode_symlink(dir, dentry, oldname);
    	if (error)
    		goto exit_lock;
    
    	DQUOT_INIT(dir);
    	lock_kernel();
    	error = dir->i_op->symlink(dir, dentry, oldname);
    	unlock_kernel();
    
    exit_lock:
    	up(&dir->i_zombie);
    	if (!error) {
    		inode_dir_notify(dir, DN_CREATE);
    		security_ops->inode_post_symlink(dir, dentry, oldname);
    	}
    	return error;
    }

     

    Communication avec le user space

    Ajouter des labels ou autres aux objets du système se fait assez facilement par le biais des champs de sécurité comme nous venons de le voir. Toutefois, cela se passe uniquement en espace noyau. Pourtant, un utilisateur pourrait très bien souhaiter connaître, si la politique de sécurité le permet, ses propres permissions, ou celles nécessaires sur certains objets. On imagine aisément une commande lsperm, renvoyant les permissions.
    Pour réaliser cela, il apparaît clairement qu'une interface est nécessaire entre l'espace utilisateur (là où est lancé la pseudo-commande lsperm) et l'espace noyau (là où sont stockées les informations de sécurité).
    Comme il n'existe pas 50 manières de faire communiquer kernel space et user space, la première solution envisagée fut la mise en place d'un appel système sys_security(). Celle-ci ne fut finalement pas retenue pour éviter de mettre en place un appel système non standard d'une part, mais surtout qui servirait à tout et rien puisque sa fonction serait propre à chaque module.
    La solution retenue actuellement repose sur le système de fichiers /proc, mais chaque LSM est libre de faire comme bon lui semble. La charpente intègre dans la structure security_operations deux pointeurs sur fonction destinés à communiquer avec le user space via le /proc :

    int (*getprocattr)(struct task_struct *p, char *name, void *value, size_t size);
    int (*setprocattr)(struct task_struct *p, char *name, void *value, size_t size);

    Les répertoires /proc/<pid>/ contiennent maintenant un nouveau répertoire, attr/, qui contient les entrées correspondant aux labels de sécurité que l'on souhaite connaître, ou même fixer. Ces entrées appellent directement les fonctions précédentes, avec le paramètre name valant le nom du fichier de attr/ auquel nous accédons.

    Tests de consistance

    Le plus gros problème des politiques de contrôle d'accès en général et des charpentes de politique de sécurité en particulier est d'arriver à prouver leur consistance.
    Dans notre cas, il faut parvenir à montrer que la charpente proposée par les LSM permet une telle consistance, c'est-à-dire qu'elle n'oublie pas de contrôler un point particulier qui permettrait de contourner un autre point de contrôle. Par exemple, contrôler l'appel système ioperm() ne sert à rien si l'appel iopl() ne peut pas être filtré.
    Cette consistance n'est pas réellement prouvable au sens mathématique, vu le travail que nécessiterait la modélisation du noyau Linux. En revanche, des travaux très intéressants ont été menés par Zhang et al. [ZEJ02], pour arriver à un degré de confiance correct en cette charpente.
    Cette vérification se déroule en deux étapes. Tout d'abord, il s'agit de s'assurer de la complétude du principe de médiation (complete mediation, aussi appelée reference monitor property) [SS75] : le mécanisme de contrôle d'accès doit être capable d'intercepter et potentiellement d'empêcher tous les accès à une ressource. Dans le cas contraire, la sécurité ne peut être garantie. Prenons comme analogie un firewall qui protège un réseau interne d'Internet : si une seule route permet d'atteindre un élément du réseau interne sans passer par le firewall, alors ce dernier ne sert à rien.
    Il faut donc que chaque objet subisse bien une opération de contrôle de la part d'un hook fourni par les LSM avant d'être utilisé. Seuls les objets accessibles en espace utilisateur sont impliqués ici comme nous l'avons vu précédemment.
    Cette vérification est effectuée à l'aide des opérations suivantes :

    • 1. détermination des fonctions qui initialisent les objets ;
    • 2. détermination des fonctions qui utilisent les objets ;
    • 3. détermination des fonctions qui autorisent l'accès aux objets ;
    • 4. vérification que toutes les manipulations d'objets dans les fonctions d'autorisation sont réalisées après le contrôle d'accès ;
    • 5. vérification qu'il n'y ait pas d'affectation de l'objet après son contrôle d'accès ;
    • 6. détermination des chemins d'exécution entre l'initiation et l'utilisation de l'objet ;
    • 7. vérification de la présence d'au moins une opération de contrôle d'accès dans chaque chemin.

    L'autre problème est de s'assurer que toutes les opérations de contrôle ont bien été effectuées avant l'utilisation d'un objet, pour tous les chemins possibles entre l'initialisation et l'utilisation : il s'agit donc de vérifier la complétude de l'autorisation. Cette opération coule relativement de source une fois le problème de médiation résolu.
    L'approche employée dans [ZEJ02] repose sur une analyse statique du noyau. Si elle ne permet pas de prouver mathématiquement la sécurité de la charpente des LSMs (personne n'est à l'abri d'une erreur de codage, pas même les kernel hackers), elle a néanmoins permis d'identifier trois types d'erreur :

    • Inconsistance de la vérification : la variable qui est vérifiée n'est pas celle qui sera utilisée par la suite. Les tests conduits par les auteurs ont ainsi permis d'identifier une condition de concurrence (race condition) pouvant conduire à une exploitation. En effet, la vérification était effectuée sur un objet de type struct file, mais la suite des opérations sur un file descriptor, à partir duquel le fichier était récupéré.
    • Modification d'un objet contrôlé sans vérification de sécurité : cela se produit lorsque qu'un objet est censé avoir été contrôlé avant utilisation, mais que cela n'a pas été le cas. En particulier, l'analyse des auteurs a permis d'identifier un problème lorsqu'une page fault se produisait : l'objet file manipulé dans ce cas n'a pas subi de contrôle d'accès. Il est toutefois passé à la fonction page_cache_read() qui appelle une sous-fonction pour lire la page en question, en prenant l'objet file en argument.

    Cet exemple illustre que la lecture d'un fichier "mmap()é" ne se soucie pas d'éventuelles modifications des attributs de sécurité du fichier, une fois celui-ci en mémoire. Cela a généré quelques mails sur la liste de développement des LSMs, dont la conclusion fut que le contrôle ne serait systématique que pour les fichiers non mmap()és, les fichiers mmap()és n'étant contrôlés qu'au moment de la lecture pour chargement.

    • Problème de typage : l'analyse statique employée par les auteurs s'appuie sur le typage des objets, qui sont soit checked, soit unchecked. Ce problème survient lorsqu'une opération est réalisée sur un objet d'un type donné, alors que c'est l'autre type qui est attendu.

    Le problème de l'approche des auteurs est un taux très élevé de faux positifs, c'est-à-dire d'endroits où l'analyseur détecte une faille alors qu'il n'y en a pas. Malgré cela, ces travaux ont permis de donner une garantie de sécurité, certes non absolue mais suffisante, à la charpente des LSMs.

    Enregistrement et empilage de modules de sécurité

    Nous avons vu qu'un module de sécurité est en fait un tableau de pointeurs sur des fonctions. Il reste à préciser au noyau qu'il faut l'utiliser. Cela se fait au chargement du module par l'appel à la fonction register_security().
    Dans certaines situations, il est souhaitable de combiner plusieurs politiques de contrôle d'accès. Pour cela, il faut pouvoir enregistrer plusieurs modules de sécurité. Lorsqu'un module est déjà enregistré, l'appel à la fonction register_security() échouera. Il est alors possible de s'enregistrer auprès du module déjà enregistré en appelant mod_reg_security(). Libre à lui d'accepter notre module sur son dos. S'il accepte, nous recevrons toutes les demandes d'autorisation qu'il souhaitera nous transmettre, mais il n'est ni obligé de nous consulter à chaque fois, ni obligé de tenir compte de notre avis.
    En résumé, le code d'enregistrement d'un LSM ressemblera le plus souvent à ça :

    /* 1 si présence d'un autre module avant */
    static int secondary;
    
    /* définition des opérations de notre charpente */
    static struct security_operations facist_ops = {
            ...
    };
    
    static int init_module (void)
    {
           ...
           /* enregistrement dans la charpente */
           if (register_security (&facist_ops)) {
                   printk (KERN_INFO "échec de l'enregistrement\n");
                   /* tentative en contactant le module précédent */
                   if (mod_reg_security (MY_NAME, &facist_ops)) {
                           printk (KERN_INFO "échec auprès module précédent\n");
                           return -EINVAL;
                   }
                   secondary = 1;
           }
           printk (KERN_INFO "facist LSM chargé\n");
           return 0;
    }

    Dans la même lignée, le déchargement du module dépend de la position dans la chaîne de sécurité :

    static void exit_module (void)
    {
           /* on se retire de la charpente */
           if (secondary) {
                   if (mod_unreg_security (MY_NAME, &facist_ops))
                           printk (KERN_INFO "échec du déchargement secondaire\n");
                   return;
           }
    
           if (unregister_security (&facist_ops)) {
                   printk (KERN_INFO "échec du déchargement\n");
           }
    }

    Lorsque plusieurs modules sont chargés, il faut voir cela comme une liste. Chaque module est alors en charge d'appeler, s'il le souhaite, les contrôles du module suivant.
    Par exemple, si un module souhaite interdire systématiquement le reboot sans en référer à un éventuel successeur, la fonction de contrôle sera :

    static int facist_reboot (unsigned int cmd)
    {
           return -EPERM;
    }

    En revanche, s'il souhaite suivre l'avis d'un éventuel successeur, la fonction ressemblera à :

    /* pointeur sur les hooks du successeur */
    struct security_operations *next_ops;
    
    static int facist_reboot (unsigned int cmd)
    {
            int res = -EPERM;
            if (next_ops->reboot)
                    res = next_ops->reboot(cmd);
            return res;
    }

    Mais il faut alors prévoir que cet éventuel successeur aura besoin de s'enregistrer :

    static struct security_operations *next_security_ops;
    
    static int mylsm_register(const char *name, struct security_operations *ops)
    {
            int res = 0;
            MOD_INC_USE_COUNT;
    	if (next_security_ops) {
    		res = next_security_ops-register_security(name, ops);
    		printk(KERN_INFO "asked for %s registration. returned %d.\n",
                           name, res);
    	}
    	else {
            	next_security_ops = ops;
            	printk(KERN_INFO "Registering security module %s.\n", name);
            }
    	if (res) MOD_DEC_USE_COUNT;
          	return res;
    }
    
    static int mylsm_unregister(const char *name, struct security_operations *ops)
    {
            int res = -EINVAL;
    	if (!next_security_ops) {
    		printk(KERN_INFO "no secondary module %s.\n", name);
    	} else if (ops == next_security_ops) {
    		next_security_ops = NULL;
    		printk(KERN_INFO "unregistering module %s.\n", name);
    		res = 0;
    	} else res = next_security_ops-unregister_security(name, ops);
            if (!res) MOD_DEC_USE_COUNT;
            return res;
    }

    La pratique

    Ça a l'air bien, je veux essayer

    Pour les noyaux 2.4, il suffit de télécharger le patch [lsm] et de l'appliquer au noyau correspondant. Pour les noyaux 2.6, les LSM sont en standard, donc il n'y a rien d'autre à faire que de télécharger son noyau.

    Les répertoires

    Le patch LSM va, en plus des ancres réparties un peu partout dans le code, rajouter un fichier d'includes include/linux/security.h, qui définit la structure struct security_operations, ainsi que déclarer l'existence d'une demi-douzaine de fonctions, dont celles nécessaires pour s'enregistrer auprès de la charpente ou d'un module de sécurité.
    Le reste du code est placé dans le répertoire security, à la racine de l'archive. Il contient le Makefile et le menu de configuration Config.in (Kconfig pour les 2.6), le fichier security.c qui implémente les fonctions annoncées dans include/security.h, le module dummy.c qui est la politique par défaut, et capability.c qui implémente les capabilities.
    Nous avons également quelques exemples ou projets, qui varient selon les patches. Pour les noyaux 2.4, on y trouve un portage de certaines fonctionnalités d'Openwall dans owlsm.c, et DTE, LIDS et SE Linux dans leurs répertoires respectifs. Pour les 2.6, on y trouve seulement SE Linux et root_plug, un petit exemple qui permet d'interdire les processus de tourner avec un egid valant 0 si une clef USB n'est pas branchée.

    Les modules de sécurité

    Nous venons d'énumérer les modules de sécurités présents. Le seul qui soit vraiment important est dummy.c, car il implémente le contrôle d'accès par défaut, c'est-à-dire celui qui est appliqué lorsqu'aucun module n'est chargé.
    Le module capability.c est aussi important car il implémente les contrôles qui manquent à dummy.c pour retrouver un contrôle d'accès tel qu'on le connaît dans un noyau sans LSM.

    Application : création d'un jeu de règles rudimentaire

    Afin d'illustrer notre propos, nous avons créé un petit LSM, dictator, dont le rôle est de restreindre l'accès à certains objets en fonction du sujet. En fait, cela est bien plus simple (et notre approche bien plus simpliste) qu'il ne semble. Les sources complètes du module sont disponibles à http://www.security-labs.org/Download/dictator.tgz.

    Les structures

    Comme une des caractéristiques des dictateurs est d'appliquer des lois à la tête du client, nous allons faire de même. Nous définissons quelques structures dont nous avons besoin pour décrire nos règles :

    /* Define the structure for the rules  */
    struct restriction {
        char *   r_name;
        u_long   r_inode;
        kdev_t   r_dev;
        u_int    r_perms;
    };
    
    /* These mean this right is removed */
    #define DONT_READ   0x1
    #define DONT_WRITE  0x2
    #define DONT_EXEC   0x4
    
    /* (Capabilities like, i.e. sorted by subjects) */
    struct laws {
        uid_t l_uid;                    /* uid to restrict */
        struct restriction *l_rules;    /* inodes uid cant access */
    };

    La structure restriction correspond en fait à une loi. Elle indique les permissions (r_perms) qu'on retire à un inode donné (r_inode, r_dev). Le nom est uniquement là pour faire joli lors des affichages des logs car nous ne pouvons pas l'utiliser si nous voulons que notre politique de sécurité soit fiable.
    En effet, plusieurs fichiers peuvent avoir le même nom. Le seul identifiant unique pour un fichier est son numéro d'inode sur un device donné, c'est pourquoi nous fondons notre politique dessus. Il est possible d'obtenir ces informations par la commande ls :

    • l'option -i donne le numéro d'inode d'un fichier ;
    • dans le répertoire des devices, l'option -l affiche les numéros de majeur et mineur servant au calcul du numéro de device.

    Par exemple :

    phil:~$ mount
    [...]
    /dev/sda2 on /tmp type ext3 (rw,errors=remount-ro)
    [...]
    phil:~$ ls -al /dev/sda2
    brw-rw----    1 root     disk       8,   2 Apr 15  2001 /dev/sda2
    phil:~$ touch /tmp/toto
    phil:~$ ls -i /tmp/toto
         38 /tmp/toto

    permet de conclure que, pour l'arborescence telle qu'elle est montée, le couple (device=0x0802, inode=38) représente de manière unique les données contenues dans le fichier, ou plutôt devrait-on dire l'inode. Un lien sur ce fichier aura le même inode, donc nous sommes à l'abris des chemins détournés. Ce qui est important, après tout, ce sont les données, et pas le nom qu'on utilise pour y accéder.
    Pour éviter de remplir les informations à la main, nous définissons également la fonction init_policies() qui initialise les règles définies en transformant le nom de fichier en paire (r_inode, r_dev) (cf ci-après). Si une des règles contient une entrée qui n'existe pas, le module n'est pas chargé, et une erreur est émise vers les logs.

    Les politiques à la tête du client

    Rappelez vous que les LSMs sont restrictifs. Nous allons donc nous contenter d'ajouter des interdits supplémentaires.
    Pour l'utilisateur joker (uid=500) : tout d'abord, nous protégeons le répertoire /home/bwayne/tmp en interdisant l'accès en lecture ou écriture. De plus, nous bloquons tout accès au fichier /home/bwayne/secret. Enfin, pour être tranquille, nous interdisons aussi l'accès à /etc/shadow :

    static struct restriction joker_rules[] = {
    	/* dir/file                inode  dev  removed perms                         */
    	{"/home/bwayne/tmp",           0,   0, DONT_READ|DONT_WRITE},
    	{"/home/bwayne/secret",        0,   0, DONT_READ|DONT_WRITE},
    	{"/etc/shadow",                0,   0, DONT_READ|DONT_WRITE|DONT_EXEC},
    	{0,                            0,   0, 0} /* trick to have a null at the end in find_rule_* */
    };
    
    static struct laws joker_policy = {
    	.l_uid   =  500,
    	.l_rules = joker_rules
    };

    De même, on définit une politique pour root, en lui interdisant l'accès aux homedir de ses utilisateurs :

    static struct restriction root_rules[] = {
    	/* dir/file         inode  dev  removed perms                         */
    	{"/home",               0,   0, DONT_READ|DONT_WRITE|DONT_EXEC},
    	{0,                     0,   0, 0} /* trick to have a null at the end in find_rule_* */
    };
    
    static struct laws root_policy = {
    	.l_uid  = 0,
    	.l_rules = root_rules
    };

    Nous pouvons maintenant définir la politique à appliquer sur le système. La politique par défaut ci-après ne sert à rien si ce n'est à indiquer que c'est la dernière "politique à la tête du client" disponible.

    static struct restriction default_rule[] = {
            {0,                     0,   0, 0}
    };
    
    static struct laws default_policy = {
    	.l_uid   = -1,
    	.l_rules = default_rule
    };
    
    /* politiques pour les utilisateurs */
    static struct laws *policy[] = {
    	&joker_policy,
    	&root_policy,
    	&default_policy /* must ALWAYS be the last one */
    };

     

    Mise en place

    Maintenant que nous avons tout ce qu'il nous faut, il nous reste à choisir les opérations que notre dictateur va superviser. Comme il est très simple, on se contente de tout ce qui est lié aux inodes, et nous allons donc définir notre comportement dans le hook dictator_inode_permission().
    Pourquoi ce choix ? En fait, ce hook est utilisé pour presque toutes les opérations liées aux inodes, ou plus exactement dès qu'on veut accéder à un inode, avant même son ouverture. Ainsi, qu'on veuille se déplacer dans un répertoire ou créer un lien vers un fichier, on passera forcément par ce hook.
    Il existe d'autres points de contrôle pour des opérations spécifiques (création de lien, effacement de fichier, etc.) ou bien pour la manipulation de fichiers. Mais après tout, nous construisons un dictateur, et il ne veut pas qu'on puisse même ouvrir un inode s'il a décidé de l'interdire.
    Dernière précision : pourquoi ne pas utiliser dictator_file_permission() ? En fait, ce dernier n'est appelé que dans le cas d'une lecture ou d'une écriture dans un fichier. Notre dictateur est plus strict et général que ça.

    static int dictator_inode_permission (struct inode *inode, int mask)
    {
    	struct laws *law;
    	struct restriction *rule;
    
    	if (!(S_ISLNK(inode->i_mode) || S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)))
    		return 0;
    
    	get_policy(current->euid, law);
    
    	/* permissive default policy */
    	if (law == &default_policy) {
    		return 0;
    	}
    
    	get_rule_by_inode(inode->i_ino, inode->i_dev, law, rule);
    	if (rule->r_name) {
    		check_perm(mask & MAY_READ, rule->r_perms & DONT_READ, "read", rule->r_name);
    		check_perm(mask & MAY_WRITE, rule->r_perms & DONT_WRITE, "write", rule->r_name);
    		check_perm(mask & MAY_EXEC, rule->r_perms & DONT_EXEC, "exec", rule->r_name);
    	}
    	return 0;
    }

    La macro get_policy() initialise le pointeur law en fonction de l'uid passé en argument. La macro get_rule_by_inode() initialise le pointeur rule en fonction du numéro d'inode et de device auquel on tente d'accéder. Si on trouve une règle à appliquer, la macro check_perm() prend la décision finale en fonction des droits. Si l'accès est interdit, elle retourne -EPERM (0 si aucun problème).

    En route vers le pouvoir

    Une fois compilé et chargé, regardons ce qui se passe. Nous avons bien pris soin de mettre dans nos macros des printk() pour nous faire part des décisions prises.

    root@gotham# insmod dictator.o && tail -f /var/log/kernel/all
    08:54:08 gotham kernel: init policy for uid=500
    08:54:08 gotham kernel: path_walk() succeed for (/home/bwayne/tmp 43402 2049)
    08:54:08 gotham kernel: path_walk() succeed for (/home/bwayne/secret 43406 2049)
    08:54:08 gotham kernel: path_walk() succeed for (/etc/shadow 43415 2049)
    08:54:08 gotham kernel: init policy for uid=0
    08:54:08 gotham kernel: path_walk() succeed for (/home 14911 2049)
    08:54:08 gotham kernel: dictator LSM initialized (second=0)

    En tant que root, nous tentons une approche subtile dans le répertoire /home/joker :

    root@gotham# cd /home/joker
    bash: cd: /home/joker: Operation not permitted
    Aug 24 09:03:18 gotham kernel: dictator_inode_permission exec denied for (0, /home, 14911, 2049)

    La sanction ne se fait pas attendre. Vous remarquerez que nous avions simplement interdit l'accès à /home avec l'inode adéquate, mais que cette interdiction porte également sur les répertoires situés en-dessous. En fait, cela est dû à la fonction link_path_walk() (dans fs/namei.c), qui parcourt chaque élément du chemin en vérifiant si les permissions nécessaires sont présentes (fonctions permission() dans fs/namei.c). Or, notre ancre est justement située dans cette fonction : nous n'avons donc pas besoin de nous préoccuper de l'arborescence et nous interdisons d'un coup un nœud et ses descendants (quel cruel dictateur n'est-ce pas ? ;-)
    En tant qu'utilisateur joker maintenant, tentons d'aller voler le secret caché dans /home/bwayne/secret :

    joker@gotham$ cat /home/bwayne/secret
    cat: /home/joker/secret: Operation not permitted
    Aug 24 09:04:16 gotham kernel: dictator_inode_permission read denied for (500, /home/bwayne/secret, 43406,  2049

    En revanche, si nous pouvons bien entrer dans le répertoire /home/bwayne/tmp, nous ne pouvons rien faire dedans :

    joker@gotham$ cd /home/bwayne/tmp
    Aug 24 09:43:51 gotham kernel: dictator_inode_permission exec granted for (500, /home/bwayne/tmp, 43402, 2049)
    joker@gotham$ ls
    ls: .: Operation not permitted
    Aug 24 09:44:50 gotham kernel: dictator_inode_permission read denied for (500, /home/bwayne/tmp, 43402, 2049)

    Dernier point, tentons maintenant de lire le fichier de mots de passe :

    joker@gotham$ cat /etc/shadow
    cat: /etc/shadow: Permission denied

    Certes, l'accès nous est interdit... mais rien dans les logs. Bien sûr, cela est normal. Les droits du fichiers sont 600, donc l'utilisateur joker ne peut pas lire le fichier. Donc l'accès est refusé bien avant d'arriver dans notre point de contrôle. Rappelez-vous, les points de contrôle sont testés en dernier.
    Ce module n'est pas du tout convivial et performant : pour le moment, il n'a que 2 utilisateurs et très peu de restrictions. Donc, l'impact n'est pas trop lourd. Mais je vous laisse imaginer ce que ça pourrait être avec beaucoup plus de contraintes...
    Pour les curieux qui se demandent encore quel est ce secret que bwayne cherche à protéger, voici la solution :

    bwayne@gotham$ cat /home/bwayne/secret
    batman

    Conclusion

    Ce tour d'horizon des LSMs s'achève. La "philosophie" des LSMs est de permettre, pas d'imposer. Si vous ne souhaitez pas les activer dans votre noyau, il suffit de le faire lors de la sélection des options. Et au pire, si c'est activé et que vous n'en voulez pas, vous avez les modules dummy et capability pour vous retrouver en terrain connu. Dans les noyaux 2.6, vous pouvez choisir de n'activer que certains hooks (système de fichiers ou réseau).
    Bien évidemment, l'ajout de ces points de contrôle a un impact sur les performances, dont [WC02] donne une estimation sur 3 tests (microbenchmark : LMBench, macrobenchmark : kernel compilation, macrobenchmarks : Webstone). Signalons que ces tests ont été réalisés sur un noyau 2.5.15, et que les choses ont dû évoluer depuis. Dans l'ensemble, les pertes sont inférieures à 2%. En revanche, la manipulation de fichiers (open(), close(), unlink(), stat()) subit un impact légèrement supérieur, ce qui se comprend avec notre module dictator. En effet, comme nous l'avons montré, lors de la résolution du chemin pour accéder à un inode, les permissions de chaque élément sont testées. L'autre perte concerne le protocole TCP : la fonction select() perd 5% et les sockets TCP 8.6%. En conséquence, le taux de connexion est également touché par cette perte de performance (dans les 7% de charge en plus, qui passent même à 16% avec SELinux). Mais répétons-le : ces tests sont vieux et mériteraient sans doute d'être ré-évalués.
    Enfin, pour conclure, ne croyez pas que les LSMs vont résoudre tous les problèmes de sécurité possibles et imaginables. D'abord, personne n'est à l'abri d'une erreur de configuration, et quand on voit la finesse de certaines charpentes, c'est loin d'être facile. Ensuite, il est des choses qu'un LSM ne fait pas. Par exemple, les protections dites "segment non exécutable" (PaX ou OpenWall) ou l'ASLR (Address Space Layout Randomization) ne rentrent pas dans ce cadre-là, tout comme certaines modifications introduites dans GRSecurity [GRSecurity] (renforcement du chroot, amélioration de l'aléa...).

    Bibliographie

    • [dte] Domain And Type Enforcement For Linux
    • http://www.cs.wm.edu/~hallyn/dte/
    • [flask] FLASK
    • http://www.cs.utah.edu/flux/fluke/html/flask.html
    • [Biba77] K.J. Biba. Integrity considerations for secure computer systems. Rapport technique 3153 du MITRE, 1977.
    • [BLP73] D. E. Bell et L. J. LaPadula. Secure computer systems: a mathematical model. Rapport technique 2547 du MITRE, vol. II, 1973.
    • [GRSecurity] B. Spengler - GRSecurity
    • http://www.grsecurity.org
    • [Lampson74] B. W. Lampson. Protection. ACM Operating Systems Rev., 8(1) : p. 18-24, 1974.
    • [lids] LIDS homepage
    • http://www.lids.org
    • [lsm] Linux Security Modules homepage
    • http://lsm.immunix.org/
    • [LSMIntro] Linux Security Modules: General Security Hooks for Linux.
    • http://lsm.immunix.org/docs/overview/linuxsecuritymodule.html
    • [medusa] Medusa DS9 Security System
    • http://medusa.fornax.sk/
    • [rsbac] Rule Set Based Access Control (RSBAC) for Linux
    • http://www.rsbac.org
    • [SC96] R. S. Sandhu, E. J. Coyne, H. L. Feinstein et C. E. Youman. Role-based access control models. IEEE Computer, 29(2) : p. 38-47, 1996.
    • [selinux] Security-Enhanced Linux
    • http://www.nsa.gov/selinux/
    • [SS75] J. H. Saltzer et M. D. Schroeder. The protection of information in computer systems. In Proc. of the IEEE, 9(63) : p. 1278-1308, 1975.
    • [subdomain] SubDomain
    • http://www.immunix.org/subdomain.html
    • [WC02] C. Wright, C. Cowan, J Morris, S. Smalley, G. Kroah-Hartman. LSM: General Security Support for the Linux Kernel. In Proc. of USENIX 2002.
    • http://lsm.immunix.org/docs/lsm-usenix-2002/
    • [ZEJ02] X. Zhang, A. Edwards et T. Jaeger. Using CQUAL for Static Analysis of Authorization Hook Placement. In USENIX Security Symposium, San Francisco, CA, August 2002. 

    Retrouvez cet article dans : Linux Magazine Hors série 17

    Posté par (La rédaction) | Signature : Frédéric RAYNAL, Philippe BIONDI | Article paru dans

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.

    • Il y a actuellement

    • 861 articles/billets en ligne.